diff --git a/pyproject.toml b/pyproject.toml index 0bfcc967a0..ae2e992f69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "omnimarket" -version = "0.4.199" +version = "0.4.200" description = "OmniMarket - Portable ONEX workflow package registry" authors = [ {name = "OmniNode.ai", email = "contact@omninode.ai"} diff --git a/src/omnimarket/nodes/node_pr_lifecycle_fix_effect/handlers/occ_evidence_stamp.py b/src/omnimarket/nodes/node_pr_lifecycle_fix_effect/handlers/occ_evidence_stamp.py index 81c5c8e984..82490aff7b 100644 --- a/src/omnimarket/nodes/node_pr_lifecycle_fix_effect/handlers/occ_evidence_stamp.py +++ b/src/omnimarket/nodes/node_pr_lifecycle_fix_effect/handlers/occ_evidence_stamp.py @@ -119,7 +119,7 @@ is_seam_ticket: false interface_change: false interfaces_touched: [] - evidence_requirements: + {requirements_block}evidence_requirements: - kind: "ci" description: "PR #{pr_number} product diff scope present" command: "gh pr view ${{PR_NUMBER}} --repo ${{REPO}} --json files" @@ -915,6 +915,94 @@ def render_ac_bindings_block( return " binds_ac:\n" + labels + " ac_bindings:\n" + "".join(entries) +#: The id of the single requirement the transcriber emits. One requirement +#: holding every transcribed criterion, not one per criterion: a requirement is +#: a statement of what is wanted and the criteria are how it is settled, and +#: the transcriber has exactly one thing to say -- "these are the criteria this +#: ticket's author declared". Splitting them would invent a requirement +#: boundary nobody wrote. +TRANSCRIBED_REQUIREMENT_ID = "req-transcribed-acceptance-criteria" + +#: ``ModelRequirement.statement`` is required and rejects whitespace. This says +#: what the block IS, including the direction of authority, so a reader of the +#: contract is not left to infer that these words came from the tracker. +TRANSCRIBED_REQUIREMENT_STATEMENT = ( + "The acceptance criteria this ticket's author declared, transcribed from " + "the ticket body at autobind time by occ-autobind. The ticket body is the " + "source and this block is the copy; each criterion's id is the label the " + "sibling binds_ac entry claims." +) + +#: Indents inside the ``requirements:`` block. ``acceptance`` entries sit two +#: levels below the top-level key, so a criterion's ``statement`` renders at 8 -- +#: the same budget ``render_check_value_field`` was measured against for a +#: contract's ``check_value``, which is why its default applies unchanged. +_REQUIREMENT_STATEMENT_INDENT = 4 +_CRITERION_STATEMENT_INDENT = 8 + + +def render_requirements_block( + records: Sequence[ModelTranscribedBinding], +) -> str: + """The top-level ``requirements:`` block for a transcribed companion. + + OMN-19038. ``binds_ac`` names the criteria an evidence item claims and + ``ac_bindings`` pins each one by hash, but neither carries the criterion's + WORDS. Measured on ``origin/dev`` at ``605ebfd729``: of 9,300 contracts, + 370 carry a ``binds_ac`` entry and one carries ``requirements`` -- so the + OMN-18270 serializer, which renders ``requirements[].acceptance[]`` into + the ticket body and treats the contract as the model, had essentially no + input anywhere in the corpus. + + The text was never missing. :func:`omnimarket.occ_criterion_units. + criterion_units` produces it as ``unit.text``, the transcriber hashes it, + and until now dropped it. This renders it. + + **Why this takes the same record list as** :func:`render_ac_bindings_block` + **and not its own.** The serializer refuses a contract that binds a label + its model does not enumerate (``ac_section_unenumerated_binding``), and the + OMN-18333 rule ``ac_binding_criterion_unbound`` refuses the converse -- a + declared criterion nothing claims. A model built from a second filter would + satisfy neither reliably, and the disagreement would be invisible at mint + time and permanent afterwards. Taking one list makes the declared set and + the bound set equal BY CONSTRUCTION rather than by agreement between two + pieces of code that will drift. + + Returns the empty string for no records, so a ticket that declared no + falsifiers mints a contract byte-identical to the one it minted before this + function existed. That is most of them. + + Each ``statement`` renders through :func:`render_check_value_field`, which + emits a literal block scalar (``|-``) for any value whose quoted form + yamlfmt would refold. Criterion text is prose and routinely long, so this + is the normal path rather than the exception; a plain scalar there gets the + formatter's sentinel injected into the value and the OMN-15479 + contamination ratchet then rejects the file, correctly. + """ + if not records: + return "" + lines = [ + "requirements:\n", + f' - id: "{TRANSCRIBED_REQUIREMENT_ID}"\n', + render_check_value_field( + "statement", + TRANSCRIBED_REQUIREMENT_STATEMENT, + indent=_REQUIREMENT_STATEMENT_INDENT, + ), + " acceptance:\n", + ] + for record in records: + lines.append(f' - id: "{record.label}"\n') + lines.append( + render_check_value_field( + "statement", + record.statement, + indent=_CRITERION_STATEMENT_INDENT, + ) + ) + return "".join(lines) + + def render_behavior_proof_dod_evidence_item( *, repo: str, @@ -1393,7 +1481,7 @@ def render_compute_contract_summary_field(*, repo: str, pr_number: int) -> str: {summary_field}is_seam_ticket: false interface_change: false interfaces_touched: [] - evidence_requirements: + {requirements_block}evidence_requirements: - kind: "ci" description: "PR #{pr_number} CI checks green" command: "gh pr checks ${{PR_NUMBER}} --repo ${{REPO}}" @@ -2209,6 +2297,11 @@ def render_companion_contract( _CONTRACT_HEAD_TEMPLATE.format( ticket_id=ticket_id, pr_number=pr_number, + # OMN-19038. The SAME record list that renders binds_ac below, so + # the model the serializer reads and the bindings the gate resolves + # are two renderings of one filtered unit list. Empty renders the + # empty string, which is the pre-OMN-19038 head byte for byte. + requirements_block=render_requirements_block(ac_bindings), # The ONE derivation, computed once above and consumed by both the # declaration and the executed item, so the contract's stated # requirement and its executed check cannot disagree. @@ -2606,6 +2699,12 @@ def render_compute_downstream_dod_evidence_item( summary_field=render_compute_contract_summary_field( repo=repo, pr_number=pr_number ), + # Empty by construction, not by omission: this renderer keeps only the + # tail from the `dod_evidence:` marker onward, so anything rendered + # into the head's preamble is discarded. A repair row appends ONE + # evidence item to a contract that already exists; it does not + # re-author that contract's model. + requirements_block="", behavior_evidence_requirement=render_behavior_evidence_requirement( repo=repo, pr_number=pr_number, changed_files=() ), @@ -2704,6 +2803,12 @@ def render_compute_companion_contract( summary_field=render_compute_contract_summary_field( repo=repo, pr_number=pr_number ), + # OMN-19038, wired on BOTH producers deliberately. OMN-18332 is the + # precedent: the transcription landed on this compute path, was not + # wired into the born path, and seven of the next nine companions + # minted unbound. A model emitted by one producer and not the other + # would reproduce that exactly, one layer up. + requirements_block=render_requirements_block(ac_bindings), behavior_evidence_requirement=render_behavior_evidence_requirement( repo=repo, pr_number=pr_number, changed_files=changed_files ), diff --git a/src/omnimarket/occ_ac_transcription.py b/src/omnimarket/occ_ac_transcription.py index 5c336867cd..25b7468db9 100644 --- a/src/omnimarket/occ_ac_transcription.py +++ b/src/omnimarket/occ_ac_transcription.py @@ -90,6 +90,21 @@ class ModelTranscribedBinding(BaseModel): "digest matches nothing the gate computes." ), ) + statement: str = Field( + ..., + description=( + "The criterion's own words, canonicalised exactly as " + "`canonical_criterion_text` produced them for the hash above. " + "OMN-19038: this text was already in hand at every mint and was " + "discarded, so the corpus held 370 bindings and one copy of the " + "criteria they name. It is REQUIRED and has no default, because a " + "binding whose statement defaulted to empty would render a model " + "declaring a criterion with no content -- which reads as an " + "enumerated criterion to the serializer and is worse than the " + "absence it replaced." + ), + min_length=1, + ) falsifier: str = Field( ..., description="The check the author declared would settle this criterion.", @@ -217,6 +232,11 @@ def transcribe_ac_bindings( ModelTranscribedBinding( label=unit.label, criterion_hash=pinned, + # OMN-19038. The text the hash above was taken over, carried + # rather than dropped. It comes from the SAME `unit`, so the + # statement and the digest can never describe different + # sentences. + statement=unit.text, falsifier=unit.falsifier, proposed_by=proposed_by, accepted_by=acceptor if may_accept else None, diff --git a/tests/unit/occ/test_omn_18332_born_path_transcription.py b/tests/unit/occ/test_omn_18332_born_path_transcription.py index 1808821a80..397858ade8 100644 --- a/tests/unit/occ/test_omn_18332_born_path_transcription.py +++ b/tests/unit/occ/test_omn_18332_born_path_transcription.py @@ -49,6 +49,10 @@ def _accepted(label: str) -> ModelTranscribedBinding: return ModelTranscribedBinding( label=label, criterion_hash=_hash(f"criterion {label}"), + # OMN-19038 made the criterion's own text a required field, so these + # fixtures now carry it. The bindings this module asserts on are + # unchanged; the statement simply travels beside them. + statement=f"{label}: criterion {label} -- falsifier: pytest -k {label}", falsifier=f"pytest -k {label}", proposed_by=AUTOBINDER_IDENTITY, accepted_by="user-id-of-the-author", @@ -60,6 +64,7 @@ def _draft(label: str) -> ModelTranscribedBinding: return ModelTranscribedBinding( label=label, criterion_hash=_hash(f"criterion {label} as edited"), + statement=f"{label}: criterion {label} as edited -- falsifier: pytest -k {label}", falsifier=f"pytest -k {label}", proposed_by=AUTOBINDER_IDENTITY, ) diff --git a/tests/unit/occ/test_omn_18332_creation_revision_transcription.py b/tests/unit/occ/test_omn_18332_creation_revision_transcription.py index 2bf02da1f0..88661dbebe 100644 --- a/tests/unit/occ/test_omn_18332_creation_revision_transcription.py +++ b/tests/unit/occ/test_omn_18332_creation_revision_transcription.py @@ -588,6 +588,11 @@ def test_the_model_refuses_a_half_populated_acceptance(self) -> None: ModelTranscribedBinding( label="AC1", criterion_hash=hashlib.sha256(b"x").hexdigest(), + # Populated so the half-acceptance rule is what fails here. + # Without it (OMN-19038 made the field required) this raised a + # missing-field error instead and the test passed on the wrong + # exception. + statement="AC1: the thing holds -- falsifier: run it", falsifier="run it", proposed_by=AUTOBINDER_IDENTITY, accepted_by=AUTHOR, diff --git a/tests/unit/occ/test_omn_19038_requirements_transcription.py b/tests/unit/occ/test_omn_19038_requirements_transcription.py new file mode 100644 index 0000000000..28f26d860a --- /dev/null +++ b/tests/unit/occ/test_omn_19038_requirements_transcription.py @@ -0,0 +1,365 @@ +# SPDX-FileCopyrightText: 2026 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""OMN-19038. The criterion TEXT survives the mint, not just its digest. + +The transcriber reads the ticket body, splits it into criterion units, and +hashes each one. Then it throws the words away: ``ModelTranscribedBinding`` +carried ``label``, ``criterion_hash`` and ``falsifier``, and ``unit.text`` -- +the only copy of the criterion's actual sentence anywhere in the pipeline -- +reached no field of the rendered contract. + +Measured on ``origin/dev`` 2026-09-21 at ``605ebfd729``: 9,300 contracts under +``onex_change_control/contracts/``, **370** carrying a ``binds_ac`` entry, and +exactly **one** carrying a ``requirements`` key. So 370 live bindings name +criteria whose text exists nowhere in the contract corpus, and the OMN-18270 +serializer -- which renders ``requirements[].acceptance[]`` into the ticket +body and treats the contract as the model -- has almost no input. + +This module pins the fix at the producer. The load-bearing property is not +"a ``requirements`` key is present"; a renderer that emitted an empty one, or +one built from a different filter than the bindings, would satisfy that and +would be worse than nothing, because the serializer refuses a contract binding +a label its model does not enumerate (``ac_section_unenumerated_binding``) and +a model that disagrees with its bindings turns that refusal into a permanent +one. The property is: + + ``binds_ac`` and ``requirements[].acceptance[]`` are two renderings of ONE + filtered unit list, so the declared set and the bound set are equal BY + CONSTRUCTION and cannot drift. + +``TestDeclaredSetEqualsBoundSet`` asserts that as an identity over the parsed +contract rather than as two presence checks, because two presence checks pass +on any overlap at all. +""" + +from __future__ import annotations + +import hashlib +from datetime import UTC, datetime + +import pytest +import yaml + +from omnimarket.nodes.node_pr_lifecycle_fix_effect.handlers.occ_evidence_stamp import ( + render_companion_contract, + render_compute_companion_contract, + render_requirements_block, +) +from omnimarket.occ_ac_transcription import ( + AUTOBINDER_IDENTITY, + ModelTranscribedBinding, + transcribe_ac_bindings, +) +from omnimarket.occ_criterion_units import DEFAULT_CRITERION_POLICY, criterion_units + +pytestmark = pytest.mark.unit + + +_CREATED_AT = datetime(2026, 9, 13, 21, 57, 10, tzinfo=UTC) + +#: A body in the shape the corpus actually carries: a heading, checkbox items, +#: a label, a falsifier per criterion. AC3 deliberately declares NO falsifier, +#: which is the population the transcriber refuses to bind. +BODY = """## Acceptance criteria + +- [ ] AC1: the gap lookup is index-backed rather than a sequential scan -- falsifier: an integration test EXPLAINs the query and fails if the plan contains Seq Scan +- [ ] AC2: the writer materialises rows again on the dev plane -- falsifier: max(window_start) advances past the frozen timestamp, read twice ten minutes apart +- [ ] AC3: somebody should look at the dashboard at some point +""" + + +def _statement_of(label: str) -> str: + """The canonical criterion text the transcriber hashes, read independently.""" + for unit in criterion_units(BODY, DEFAULT_CRITERION_POLICY): + if unit.label == label: + return unit.text + msg = f"fixture does not carry {label}" + raise AssertionError(msg) + + +def _record(label: str, statement: str) -> ModelTranscribedBinding: + return ModelTranscribedBinding( + label=label, + criterion_hash=hashlib.sha256(statement.encode("utf-8")).hexdigest(), + statement=statement, + falsifier=f"pytest -k {label}", + proposed_by=AUTOBINDER_IDENTITY, + ) + + +def _contract_with(*records: ModelTranscribedBinding) -> str: + return render_companion_contract( + ticket_id="OMN-19038", + repo="OmniNode-ai/omnimarket", + pr_number=2754, + evidence_id="dod-OmniNode-ai-omnimarket-pr-2754", + ac_bindings=records, + ) + + +def _parsed(rendered: str) -> dict[str, object]: + loaded = yaml.safe_load(rendered) + assert isinstance(loaded, dict), "a contract must parse as a mapping" + return loaded + + +def _acceptance_ids(contract: dict[str, object]) -> list[str]: + requirements = contract.get("requirements") + assert isinstance(requirements, list), ( + f"contract carries no requirements list; got {type(requirements)!r}" + ) + ids: list[str] = [] + for requirement in requirements: + assert isinstance(requirement, dict) + acceptance = requirement.get("acceptance") + assert isinstance(acceptance, list) + for criterion in acceptance: + assert isinstance(criterion, dict) + ids.append(str(criterion["id"])) + return ids + + +def _bound_labels(contract: dict[str, object]) -> list[str]: + evidence = contract.get("dod_evidence") + assert isinstance(evidence, list) + labels: list[str] = [] + for item in evidence: + assert isinstance(item, dict) + for entry in item.get("binds_ac") or []: + labels.append(str(entry)) + return labels + + +class TestTheContractCarriesTheCriterionText: + """The headline: the words reach the contract, not only the digest.""" + + def test_rendered_contract_enumerates_the_bound_criteria(self) -> None: + contract = _parsed( + _contract_with( + _record("AC1", _statement_of("AC1")), + _record("AC2", _statement_of("AC2")), + ) + ) + assert _acceptance_ids(contract) == ["AC1", "AC2"] + + def test_the_statement_is_the_text_the_transcriber_hashed(self) -> None: + """Not a paraphrase, not a truncation, not the falsifier alone. + + Asserted against ``criterion_units`` read independently from the same + body, so a renderer that wrote the label, the falsifier or an empty + string into ``statement`` fails here rather than passing a presence + check. + """ + contract = _parsed(_contract_with(_record("AC1", _statement_of("AC1")))) + requirements = contract["requirements"] + assert isinstance(requirements, list) + first = requirements[0] + assert isinstance(first, dict) + acceptance = first["acceptance"] + assert isinstance(acceptance, list) + criterion = acceptance[0] + assert isinstance(criterion, dict) + assert criterion["statement"] == _statement_of("AC1") + assert "Seq Scan" in str(criterion["statement"]) + + def test_the_requirement_itself_carries_a_non_empty_statement(self) -> None: + """``ModelRequirement.statement`` is required and must not be blank. + + A requirement rendered with an empty statement parses, reaches the + serializer, and fails validation in ``omnibase_core`` far from here. + """ + contract = _parsed(_contract_with(_record("AC1", _statement_of("AC1")))) + requirements = contract["requirements"] + assert isinstance(requirements, list) + first = requirements[0] + assert isinstance(first, dict) + assert str(first["id"]).strip() + assert str(first["statement"]).strip() + + +class TestDeclaredSetEqualsBoundSet: + """The identity, asserted as an identity. Two presence checks are not this.""" + + def test_every_bound_label_is_enumerated_and_nothing_else_is(self) -> None: + contract = _parsed( + _contract_with( + _record("AC1", _statement_of("AC1")), + _record("AC2", _statement_of("AC2")), + ) + ) + assert set(_acceptance_ids(contract)) == set(_bound_labels(contract)) + + def test_a_criterion_with_no_falsifier_is_in_neither(self) -> None: + """AC3 declares no falsifier, so the transcriber mints no binding for it. + + It must therefore be absent from the model too. A producer that + enumerated every criterion in the body while binding only the + falsifiable ones would declare a criterion nothing claims, which the + OMN-18333 rule ``ac_binding_criterion_unbound`` refuses. + """ + records = transcribe_ac_bindings( + live_description=BODY, + creation_revision=None, + created_at=_CREATED_AT, + ) + assert [record.label for record in records] == ["AC1", "AC2"] + contract = _parsed(_contract_with(*records)) + assert "AC3" not in _acceptance_ids(contract) + assert "AC3" not in _bound_labels(contract) + assert set(_acceptance_ids(contract)) == set(_bound_labels(contract)) + + def test_a_label_the_consumers_reader_cannot_resolve_is_in_neither(self) -> None: + """A zero-padded ordinal is the measured case, and it must drop from both. + + ``criterion_units`` reads ``AC01`` verbatim while the consumer's reader + canonicalises it to ``AC1``, so the pin lookup misses and the + transcriber mints nothing. The model must agree: enumerating a + criterion under a label the gate cannot find is an + ``ac_binding_unknown_criterion`` refusal waiting to happen. + """ + padded = BODY.replace("AC1:", "AC01:") + records = transcribe_ac_bindings( + live_description=padded, + creation_revision=None, + created_at=_CREATED_AT, + ) + labels = [record.label for record in records] + assert "AC01" not in labels + contract = _parsed(_contract_with(*records)) + assert "AC01" not in _acceptance_ids(contract) + assert set(_acceptance_ids(contract)) == set(_bound_labels(contract)) + + +class TestTheTranscriberCarriesTheStatement: + """The text must survive the transcriber, which is where it was dropped.""" + + def test_transcribed_records_carry_the_canonical_criterion_text(self) -> None: + records = transcribe_ac_bindings( + live_description=BODY, + creation_revision=None, + created_at=_CREATED_AT, + ) + assert [record.statement for record in records] == [ + _statement_of("AC1"), + _statement_of("AC2"), + ] + + def test_the_statement_is_single_line_so_it_renders_foldproof(self) -> None: + """``canonical_criterion_text`` collapses whitespace runs, and that matters. + + The contract renderer emits a long value as a literal block scalar, and + that rendering assumes a single line. A multi-line statement would + break the block and the OMN-15479 contamination ratchet would reject + the file, so the property is pinned here rather than discovered there. + """ + records = transcribe_ac_bindings( + live_description=BODY, + creation_revision=None, + created_at=_CREATED_AT, + ) + assert records + for record in records: + assert "\n" not in record.statement + assert record.statement == record.statement.strip() + + +class TestNothingChangesForATicketWithNothingToSay: + """The contract a ticket with no falsifiers mints must not move one byte.""" + + def test_no_bindings_renders_no_requirements_key(self) -> None: + rendered = _contract_with() + assert "requirements:" not in rendered.replace("evidence_requirements:", "") + contract = _parsed(rendered) + assert "requirements" not in contract + + def test_no_bindings_is_byte_identical_to_the_unbound_shape(self) -> None: + """A ticket that declared nothing gets the contract it always got. + + Guards the one regression this change could cause everywhere at once: + an unconditional block would add a key to every companion the producer + mints, including the overwhelming majority that carry no bindings. + """ + without = _contract_with() + assert "\nrequirements:\n" not in without + assert without.startswith("---\nschema_version:") + + +class TestTheRenderedContractIsWellFormed: + """Long prose has to survive the YAML layer and the formatter.""" + + def test_a_long_statement_renders_as_a_literal_block_not_a_fold(self) -> None: + """A folded or plain scalar is reflowed by yamlfmt, which corrupts it. + + The FRICTION recorded on OMN-19046 is explicit: a plain scalar gets the + formatter's sentinel injected INTO the value and the OMN-15479 ratchet + then rejects the file. Literal blocks are never refolded. + """ + long_statement = _statement_of("AC1") + assert len(long_statement) > 80, "fixture must exercise the long path" + rendered = _contract_with(_record("AC1", long_statement)) + assert "statement: |-" in rendered + # and it still parses back to exactly the text, chomped + contract = _parsed(rendered) + requirements = contract["requirements"] + assert isinstance(requirements, list) + first = requirements[0] + assert isinstance(first, dict) + acceptance = first["acceptance"] + assert isinstance(acceptance, list) + criterion = acceptance[0] + assert isinstance(criterion, dict) + assert criterion["statement"] == long_statement + + def test_both_producers_emit_the_identical_block(self) -> None: + """OMN-18332's lesson, applied one layer up. + + That transcription landed on the compute producer, was not wired into + the born producer, and seven of the next nine companions minted with + no bindings at all. A model emitted by one producer and not the other + reproduces that failure exactly, and a corpus census would show it + only as "some contracts have requirements". + + Asserted as equality of the rendered block rather than presence on + each side, so a second, differently-spelled renderer also fails here. + """ + records = ( + _record("AC1", _statement_of("AC1")), + _record("AC2", _statement_of("AC2")), + ) + born = _parsed(_contract_with(*records)) + compute = _parsed( + render_compute_companion_contract( + ticket_id="OMN-19038", + repo="OmniNode-ai/omnimarket", + pr_number=2754, + evidence_id="dod-OmniNode-ai-omnimarket-pr-2754", + ac_bindings=records, + ) + ) + assert born["requirements"] == compute["requirements"] + assert _acceptance_ids(compute) == ["AC1", "AC2"] + assert set(_acceptance_ids(compute)) == set(_bound_labels(compute)) + + def test_the_compute_producer_with_no_bindings_emits_no_key(self) -> None: + rendered = render_compute_companion_contract( + ticket_id="OMN-19038", + repo="OmniNode-ai/omnimarket", + pr_number=2754, + evidence_id="dod-OmniNode-ai-omnimarket-pr-2754", + ) + assert "\nrequirements:\n" not in rendered + assert "requirements" not in _parsed(rendered) + + def test_the_block_renderer_is_empty_for_no_records(self) -> None: + """The unit-level guard behind both byte-identity tests above.""" + assert render_requirements_block(()) == "" + + def test_requirements_precedes_dod_evidence_in_the_rendered_head(self) -> None: + """Position is not cosmetic: the head template owns everything above it. + + ``dod_evidence`` is appended item by item after the head, so a + ``requirements`` block emitted after it would land inside the evidence + list rather than beside it. + """ + rendered = _contract_with(_record("AC1", _statement_of("AC1"))) + assert rendered.index("\nrequirements:\n") < rendered.index("\ndod_evidence:\n") diff --git a/uv.lock b/uv.lock index 85c35083c9..46b826d38c 100644 --- a/uv.lock +++ b/uv.lock @@ -3072,7 +3072,7 @@ wheels = [ [[package]] name = "omnimarket" -version = "0.4.199" +version = "0.4.200" source = { editable = "." } dependencies = [ { name = "aiokafka" },