Skip to content

feat(wme): wire OCL invariants, preconditions, and postconditions#529

Merged
ArmenSl merged 12 commits into
developmentfrom
feature/wme-ocl-pre-post
May 6, 2026
Merged

feat(wme): wire OCL invariants, preconditions, and postconditions#529
ArmenSl merged 12 commits into
developmentfrom
feature/wme-ocl-pre-post

Conversation

@ArmenSl
Copy link
Copy Markdown
Collaborator

@ArmenSl ArmenSl commented May 5, 2026

Summary

  • Wire OCL invariants, preconditions, and postconditions into the WME class-diagram converter pipeline by routing on the parsed BOCL header (inv / pre / post).
  • Canonical wire shape is the full OCL text in the constraint textbox; method lookup is by Class::method name pair, relying on the metamodel's per-class uniqueness guarantee.
  • A back-compat shim transparently lifts body-only JSON files (saved during an intermediate iteration) to the canonical full-text shape on first ingest, then buml_to_json normalizes them on emit.
  • The validator now walks every class's methods' pre / post lists in addition to domain_model.constraints; labels include the class / method / kind so error messages point at a specific constraint box.

Changes

  • services/converters/parsers/ocl_parser.pyparse_constraint_text(text, model) extracts kind / class / method from the BOCL header and delegates lex/parse to parse_ocl(...) with an explicit context_class (the upstream auto-detect regex doesn't handle the Class::method(params) form). process_ocl_constraints now 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_constraints rewritten to coerce each box to canonical full text via _ocl_box_to_full_text, parse, and route. (class_name, method_name) -> Method index 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 walks Method.pre / Method.post directly.
  • 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

  • CI: backend tests + lint pass (python -m pytest tests/ — 1048 passed locally with 0 regressions).
  • Open the new "Library with OCL" template (frontend PR feat(wme): show OCL kind badge + add 'Library with OCL' template BESSER-Web-Modeling-Editor#124) — diagram loads with zero OCL warnings.
  • Type a typo'd pre constraint (context Library::cheapedst_book_by(author: Author) pre: ...) — /validate-diagram reports targets unknown method Library::cheapedst_book_by and the constraint is dropped (rest of the diagram still validates).
  • Save a project, reopen — every OCL box's textarea contains the full context X (inv|pre|post) name: body form (legacy body-only files normalize on first round-trip).

Related

Frontend PR: BESSER-PEARL/BESSER-Web-Modeling-Editor#124

ArmenSl added 4 commits May 5, 2026 12:57
…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.
ArmenSl and others added 8 commits May 6, 2026 13:37
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 ArmenSl merged commit 4ad99a0 into development May 6, 2026
5 checks passed
@ArmenSl ArmenSl deleted the feature/wme-ocl-pre-post branch May 6, 2026 16:21
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.
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.
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.

2 participants