feat(wme): wire OCL invariants, preconditions, and postconditions#529
Merged
Conversation
…he converter Make the WME class diagram converter route OCL constraints by their JSON ``kind`` field — invariants land on ``domain_model.constraints``; preconditions and postconditions land on ``Method.pre`` / ``Method.post`` via ``targetMethodId``. Builds on the metamodel work landed on development (PR #528): ``Method.pre`` / ``Method.post`` first-class fields, AST-backed ``OCLConstraint``, and ``parse_ocl(...)``. Backend changes: * ``parsers/ocl_parser.py`` - new ``parse_ocl_body(body, kind, name, class_, model, method=None)`` helper that synthesizes the BOCL grammar's required header (``context C inv name:`` for invariants, ``context C::m(p:T) pre|post:`` for pre/post — different shapes mandated by the BOCL grammar) before calling ``parse_ocl``. - rewritten ``process_ocl_constraints`` returns AST-backed ``OCLConstraint`` (was bare ``Constraint``) and skips malformed blocks with a warning instead of producing junk text. Kept on the legacy path for projects whose textareas still contain the full ``context X inv name: expr`` form. * ``json_to_buml/class_diagram_processor.py`` - ``_process_classes`` now returns ``(class_id_to_class, method_id_to_method)`` element-id maps; the maps are stashed on ``domain_model._class_element_ids`` and ``_method_element_ids`` so ``buml_to_json`` can re-emit the original UUIDs (without this, every BUML→JSON cycle silently broke ``targetMethodId`` references). - rewritten ``_process_constraints`` routes by ``kind``: invariants are parsed via ``parse_ocl_body`` and added to ``domain_model.constraints``; pre/post resolve their target method via ``targetMethodId`` and call ``method.add_pre`` / ``add_post``. Auto-generates names when ``constraintName`` is absent. Skip-with-warning on parse failure, missing link, or orphan target method. - the existing legacy textarea path (no ``kind`` field) continues to work. * ``buml_to_json/class_diagram_converter.py`` - reuses stashed element ids when emitting classes and methods. - existing invariant emitter tags ``kind: "invariant"`` plus ``constraintName`` so the next ingest takes the kind-aware path. - new emission pass walks every class's methods' ``pre`` / ``post`` and emits one ``ClassOCLConstraint`` element + ``ClassOCLLink`` per constraint (``kind``, ``targetMethodId``, body-only ``constraint``). * ``validators/ocl_checker.py`` - ``check_ocl_constraint`` now collects invariants AND every method's pre/post (was iterating only ``domain_model.constraints``). Labels include ``[ClassName::method kind name]`` for disambiguation. - ``OCLConstraint`` instances are accepted as valid by virtue of carrying an AST; bare ``Constraint`` still gets the lex/parse round-trip fallback. Tests: 16 new in ``test_ocl_pre_post.py`` covering parse_ocl_body header synthesis (all three kinds + grammar shapes), kind-routing, skip-with-warning on orphan targets / unlinked boxes / invalid OCL, legacy textarea compat, auto-generated naming, full JSON→BUML→JSON round-trip stability, and method-element-id stability across cycles. Full backend suite stays green (1047 passed, 0 regressions). Submodule bump: ``besser/utilities/web_modeling_editor/frontend`` -> ``feature/wme-ocl-pre-post`` (kind dropdown + method picker on the OCL constraint box).
…raints
Replace the body-only intermediate shape introduced one commit ago.
The textbox on the canvas now holds the complete OCL block — header
plus body — exactly as it appears in the BOCL grammar:
context Book inv book_pages_positive: self.pages > 0
context Book::decrease_stock(qty: int) pre: qty > 0
context Book::decrease_stock(qty: int) post: self.stock >= 0
Driver: Jordi's downstream metamodel consumer reads the full OCL source
text to extract the constraint name and detect duplicates. The body-only
shape hid the name inside metadata that his parser doesn't see, so the
canonical wire format goes back to full text.
Backend changes:
* ``parsers/ocl_parser.py``
- Drop ``parse_ocl_body``; replace with ``parse_constraint_text(text,
model)`` that inspects the BOCL header to extract kind / class /
optional method / optional name, resolves the class manually, and
delegates lex/parse to ``parse_ocl(...)`` with an explicit
``context_class`` (the upstream auto-detect regex doesn't handle
the ``Class::method(params)`` segment in pre/post headers, so we
bypass it).
- ``process_ocl_constraints`` now returns a list of
``(kind, OCLConstraint, class_name, method_name)`` routing tuples,
one per ``context X ...`` block in the textarea blob. Malformed
blocks still skip-with-warning.
- Add ``legacy_body_only_to_text(...)`` — a one-direction shim used
only on ingest, to lift JSON files saved during the previous
iteration (body-only ``constraint`` plus ``kind`` /
``targetMethodId`` / ``constraintName`` siblings) to canonical
full text before parsing. Self-deprecates per file: any project
saved through the new emission path drops the legacy fields and
never hits this shim again.
* ``json_to_buml/class_diagram_processor.py``
- Replace the kind/targetMethodId/ClassOCLLink-walk routing in
``_process_constraints`` with a single loop that:
1. coerces each box's textarea to canonical full text via
``_ocl_box_to_full_text`` (fast-path when already canonical;
legacy shim otherwise),
2. calls ``process_ocl_constraints`` to parse + classify,
3. routes invariants into ``domain_model.constraints`` and
pre/post into ``Method.pre`` / ``Method.post`` via a
``(class_name, method_name) -> Method`` index built from the
metamodel (Class.methods names are unique per class).
- Drop the ``_class_element_ids`` / ``_method_element_ids`` stashes
that existed solely to keep ``targetMethodId`` references stable
across save/load. With full-text, references are name-based and
stability is automatic.
* ``buml_to_json/class_diagram_converter.py``
- Drop ``saved_class_ids`` / ``saved_method_ids`` element-id reuse;
fresh UUIDs are fine again.
- Replace the kind/targetMethodId/constraintName fields on emitted
``ClassOCLConstraint`` boxes with a single ``constraint`` field
holding the rebuilt full text.
- Add ``_emit_full_ocl_text(constraint, kind, method=None)`` to
reconstruct the BOCL header from the constraint's context class,
name, and (for pre/post) the owning method's signature.
- Pre/post emission walks every class's methods' ``.pre`` and
``.post`` lists exactly as before — the text shape is what
changed, not the structural layout of emitted elements.
* ``parsers/__init__.py`` — re-exports updated.
Tests (test_ocl_pre_post.py): rewritten end-to-end. Drops the now-gone
``parse_ocl_body_*`` and method-element-id-stability tests; adds tests
for ``parse_constraint_text`` header extraction, multi-block parsing,
unknown-method skip-with-warning, body-only legacy ingest via the shim,
metadata stripping on emit, and full-text round-trip stability.
17 tests, all green; full backend suite (1048 passed) stays green.
Submodule bump: ``besser/utilities/web_modeling_editor/frontend`` ->
``feature/wme-ocl-pre-post`` (popup reverted to plain textarea, badge
removed).
Frontend now renders a small derived «inv»/«pre»/«post» badge on the OCL constraint box so users get a visual cue of the constraint kind without opening the popup. Pure visual; no backend impact.
Frontend ships a new structural-pattern template (``Library_OCL.json``) pre-populated with 11 canonical full-text OCL constraints across invariants, preconditions, and postconditions. Useful as a learning example and as smoke-test fodder.
5 tasks
Ruff F401 was failing CI for two leftovers from an earlier refactor: ``parse_constraint_text`` and ``BOCLSyntaxError`` were imported but never referenced in the module body.
Adds an optional `description` field to the `Constraint` metamodel so end-users see a plain-language explanation when an OCL constraint is violated during model validation, instead of the raw expression. Threaded end-to-end: - Metamodel: new `description` field on Constraint - JSON->BUML: accepts a `description` JSON field on ClassOCLConstraint elements, plus inline OCL `--` comments per constraint (inline wins) - BUML->JSON: emits `description` so editor round-trips preserve it - Validator: surfaces the description as the violation reason in /validate-diagram output (legacy format unchanged when no description) - Code builder: generated Constraint() Python code includes the field Tests: 13 new tests covering all four touch points. Note: this commit was generated by Claude Code on behalf of the user. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consolidates the OCL constraint pipeline around a single contract: ``constraint.expression`` carries the full canonical OCL text (``context X (inv|pre|post) [name]: body``) from the moment a constraint is parsed, through every storage, transport, validation, and evaluation step. No more body-only-vs-full-text divergence. Eliminates three reconstruction layers and one mutation hack that all existed to paper over OCLConstraint storing pretty-printed body-only on its expression field: - ocl_parser.parse_constraint_text: one-line override ``constraint.expression = text`` after parse_ocl(). - ocl_checker: drops _method_signature / _already_full_text / _canonical_invariant_text / _canonical_method_contract_text and the swap-and-restore around parser.evaluate(). Just uses constraint.expression directly. Adds _ensure_canonical_expression as a narrow legacy fallback for old BUML files (deprecated: removal target 2026-Q4). - buml_to_json: drops _emit_full_ocl_text. Emits constraint.expression verbatim for both class invariants and method contracts. While here: - domain_model_builder now walks each method's pre/post lists and emits ``Method.add_pre(...)`` / ``add_post(...)`` calls. Without this loop, BUML export silently dropped every method contract on round-trip — confirmed against library__full_stack__project (1).py. - json_to_buml.class_diagram_processor warns on duplicate (Class, method_name) pairs so silent overload collisions in pre/post routing are surfaced. - legacy_body_only_to_text gets a deprecation note pointing at 2026-Q4 removal. - Validator skips evaluation of pre/post against an object model (the BOCL evaluator can't bind method parameters without a runtime invocation — would raise NameError silently). Pre/post remain syntax-checked. Tests: 1061 backend tests pass (test_ocl_pre_post.py adjusted to the new ``expression`` contract). Validated against: - Library_OCL.json (7 inv + 3 pre + 1 post): 11/11 ✅ - team_player_ocl.json (2 inv): 2/2 ✅ - Library_Complete.json (1 inv): 1/1 ✅ - Legacy body-only BUML: 7/7 ✅ (via fallback) - Library_OCL → BUML → re-import → validate: 11/11 ✅, pre/post routing preserved (Book.decrease_stock.pre = 2, .post = 1).
Two ``ClassOCLConstraint`` boxes that parse to the same constraint name (whether typed by the user or auto-generated by ``process_ocl_constraints``) used to crash ``_process_constraints``: ``DomainModel.constraints`` rejects duplicate names with a ``ValueError``, and the assignment ``domain_model.constraints = set(...) | extra_invariants`` had no guard around it, so the error escaped the whole conversion. De-dup by name before assigning, keep the first occurrence, and emit a ``Warning: duplicate constraint name 'X' across OCL boxes; keeping the first occurrence`` so users can spot the collision in the validator output. Discovered by a 2000-constraint stress test (5 parallel agents exercising invariants, pre/post, multi-block, evaluator, and adversarial inputs); this was the only case that produced an uncaught exception across ~2023 inputs. Tests: new ``test_duplicate_constraint_name_across_boxes_does_not_crash`` in ``test_ocl_pre_post.py``. Full suite remains green (1062 passed).
The de-dup loop iterated over a set, whose iteration order is not guaranteed across Python versions. Locally Python 3.11 happened to yield insertion order so 'first wins' worked; CI Python 3.11 gave a different order and the second occurrence won, breaking test_duplicate_constraint_name_across_boxes_does_not_crash. Switch extra_invariants from set to list so iteration order matches the source elements.items() order (insertion order on modern Python dicts), making 'first wins' deterministic across environments.
Three independent issues called out by the multi-agent review.
1. Identifier injection via constraint/method names in BUML codegen.
``constraint.name`` and ``method.name`` flowed straight into
Python identifier positions on the LHS of assignments — a name
like ``x=1;import os;os.system('id')#`` (no spaces, no hyphens,
passes ``NamedElement.name``'s setter) would land verbatim in
code that gets ``exec()``'d. Route every identifier through
``safe_var_name`` (existing helper in ``common.py``); add a
``_method_var_name(method)`` shim and apply it at all five
emit sites (was 5 different copies of ``method.name.split('(')[0]``).
2. Pre/post boxes lost their ``description`` on JSON↔BUML round-trip.
``buml_to_json/class_diagram_converter.py`` emitted ``description``
only on the invariant path; method-contract boxes silently dropped
the field. Mirror the invariant emit. Also drops the hard-coded
``"name": "OCL"`` field that was unique to the pre/post path
(invariants don't set it; reviewer flagged the asymmetry).
3. Inline ``--`` comments lost on round-trip. ``_extract_inline_description``
used to strip ``--`` from the canonical text; the BUML→JSON
emitter never re-injected it, so a user's
``context X inv: self.x > 0 -- must be positive`` collapsed to
``context X inv: self.x > 0`` after the first emit. Now the
helper returns ``(stripped, description)`` — the parser still
sees a stripped form (``parse_ocl``'s visitor rejects trailing
comment tokens, even though the lexer skips them) — but
``process_ocl_constraints`` overrides ``constraint.expression``
with the comment-preserving canonical line so emit is bit-stable.
Tests: 1062/1062 pass. ``test_inline_comment_takes_precedence_over_default``
now asserts ``"-- Inline wins" in constraint.expression`` (the new
contract), where it previously asserted the comment was stripped.
* Embed auto-generated invariant names back into expression so the next BUML→JSON→BUML cycle parses out the same name instead of generating a fresh ``counter_blockidx`` suffix. * Tighten the OCL header regex consumer to ignore the optional name segment for pre/post — only invariants carry a name in BOCL. * Walk the inheritance chain when building the ``(class, method) → Method`` index so a precondition written as ``context Sub::base_method() pre: ...`` resolves to a method declared on a parent class instead of warn-skipping. * Narrow the residual ``except Exception`` in OCL element processing to ``(BOCLSyntaxError, ValueError)`` so programmer errors propagate instead of being swallowed. * Sort method_contracts in domain_model_builder so the emitted Python is byte-stable (matches how invariants and other emissions sort). * Drop defensive ``getattr`` / ``hasattr`` guards on metamodel attributes that the metamodel now guarantees (``Class.methods``, ``Method.pre``, ``Method.post``). * Walk method pre/post in DomainModel._validate_constraints so a precondition with a stale or external context is flagged the same way an invariant would be. * Tighten the round-trip stress test to also assert link wiring and description survive each cycle, not just constraint text. Bumps frontend submodule pointer to e906308 which adds the currentColor fallback on the OCL kind badge.
ArmenSl
added a commit
that referenced
this pull request
May 7, 2026
…Project templates - Bump setup.cfg 7.5.1 -> 7.6.0 and add v7.6.0 release notes. - Frontend submodule bump (e906308 -> ee5c9b4): also pulls in PR #126 (perspective on project create), PR #127 (Full Project tab with library_full_stack and personalized_gym_agent multi-diagram bundles), fix(workspace) diagram-bridge wire-up on project load, refactor project-import to shared/services, fix(perspectives) "Full Application" -> "Full Web Application", feat(object-diagram) generate objects+links from referenced class, chore(i18n) drop German locale, package version bump 2.5.0. - Backend: PR #528 (Method.pre/post + OCLConstraint AST/source split), PR #529 (WME wires inv/pre/post by BOCL header into Method.pre/post, validator walks every method's pre/post), PR #499 (natural-language description on Constraint), de-dup constraint names with stable insertion order, identifier-injection hardening on BUML codegen via safe_var_name, inheritance-chain method lookup for pre/post on parent classes, single canonical full-text on Constraint.expression, fix(django generator) chdir into temp_dir, fix(github deploy) surface httpx.HTTPStatusError instead of generic 500.
11 tasks
ArmenSl
added a commit
that referenced
this pull request
May 7, 2026
Closes BESSER-PEARL/B-OCL-Interpreter#5. Lets the modeling assistant author OCL constraints from a plain-language description -- e.g. "add a constraint that a Library always has at least one Book" -- by emitting a new add_ocl_constraint modification that the frontend turns into a ClassOCLConstraint element on the canvas, linked to the target class via a ClassOCLLink. - Frontend submodule bump (ee5c9b4 -> 54a0741): new ClassDiagramModifier case 'add_ocl_constraint' that builds the constraint element + link, positions the box to the right of the anchor class, and writes the BOCL block into the constraint field plus the plain-language gloss into description. Action union and ModificationChanges shape extended in modifiers/base.ts. - Modeling-agent (BESSER-PEARL/modeling-agent@c0e47e8 on main, deploy alongside via scripts/deploy.sh agent): ClassModification.action gains 'add_ocl_constraint'; class_diagram_handler system prompt gains an OCL section with 5 NL->BOCL few-shot examples and an explicit gate forbidding unprompted OCL emission. - Release notes: new "Modeling Assistant -- Natural Language to OCL" section in v7.6.0.rst covering the cross-repo wiring and the deploy callout. Pre-flight validation (calling /validate-diagram before writing) is deferred -- malformed BOCL surfaces post-hoc through the validator path landed in PR #529, which is sufficient for first iteration.
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.
Summary
inv/pre/post).Class::methodname pair, relying on the metamodel's per-class uniqueness guarantee.buml_to_jsonnormalizes them on emit.pre/postlists in addition todomain_model.constraints; labels include the class / method / kind so error messages point at a specific constraint box.Changes
services/converters/parsers/ocl_parser.py—parse_constraint_text(text, model)extracts kind / class / method from the BOCL header and delegates lex/parse toparse_ocl(...)with an explicitcontext_class(the upstream auto-detect regex doesn't handle theClass::method(params)form).process_ocl_constraintsnow returns(kind, OCLConstraint, class_name, method_name)routing tuples.legacy_body_only_to_text(...)is the back-compat shim.services/converters/json_to_buml/class_diagram_processor.py—_process_constraintsrewritten to coerce each box to canonical full text via_ocl_box_to_full_text, parse, and route.(class_name, method_name) -> Methodindex built from the metamodel for pre/post lookup.services/converters/buml_to_json/class_diagram_converter.py—_emit_full_ocl_text(constraint, kind, method=None)reconstructs the full BOCL header on emit. Pre/post emission walksMethod.pre/Method.postdirectly.services/validators/ocl_checker.py— collects invariants + every method's pre/post; label format[Class::method kind name].tests/utilities/web_modeling_editor/backend/services/converters/test_ocl_pre_post.py— new file, 17 tests covering routing, malformed-OCL skip-with-warning, legacy ingest, full-text round-trip stability.Test plan
python -m pytest tests/— 1048 passed locally with 0 regressions).context Library::cheapedst_book_by(author: Author) pre: ...) —/validate-diagramreportstargets unknown method Library::cheapedst_book_byand the constraint is dropped (rest of the diagram still validates).context X (inv|pre|post) name: bodyform (legacy body-only files normalize on first round-trip).Related
Frontend PR: BESSER-PEARL/BESSER-Web-Modeling-Editor#124