Skip to content

release: v7.6.0 — OCL pre/post end-to-end (metamodel + WME) and Full Project templates#531

Merged
ArmenSl merged 30 commits into
masterfrom
development
May 7, 2026
Merged

release: v7.6.0 — OCL pre/post end-to-end (metamodel + WME) and Full Project templates#531
ArmenSl merged 30 commits into
masterfrom
development

Conversation

@ArmenSl
Copy link
Copy Markdown
Collaborator

@ArmenSl ArmenSl commented May 7, 2026

Summary

This release lands OCL preconditions and postconditions end-to-end, from BUML metamodel through the Web Modeling Editor's class-diagram converter, validator, and canvas. Method.pre / Method.post are now first-class list fields on Method; the OCL textbox in the editor speaks the canonical BOCL form (context X (inv|pre|post) [name]: body); and the validator walks every method's pre/post lists alongside domain_model.constraints, labelling errors with [Class::method kind name]. OCLConstraint exposes the parsed ast separately from the source-text expression.

Alongside, the editor's template library gains a Full Project tab with multi-diagram bundles (Library Full Stack, Personalized Gym Agent), project creation gains modeling-perspective selection, the modeling assistant can now author OCL constraints from natural language, and a number of OCL hardening / codegen safety fixes land.

Headline changes

OCL — Metamodel and WME wiring

OCL — Editor UX (frontend)

  • Italic stereotype badge above the OCL box body — «inv», «pre» <method-name>, «post» <method-name> — derived from the BOCL header.
  • Optional description field on the OCL constraint popup; surfaces as the violation reason in validator output.
  • Constraint textbox is a single full-text field carrying the whole BOCL block; routing into Method.pre / Method.post happens entirely backend-side.
  • "Library with OCL" structural-pattern template pre-populated with eleven canonical OCL constraints.

Modeling Assistant — Natural Language to OCL (closes BESSER-PEARL/B-OCL-Interpreter#5)

  • The assistant emits a new add_ocl_constraint modification when the user explicitly asks for an invariant, precondition, or postcondition ("add a constraint that a Library always has at least one Book", "the precondition of Account::deposit is amount > 0", …).
  • Frontend ClassDiagramModifier consumes the action and spawns a ClassOCLConstraint box on the canvas linked to the target class via a ClassOCLLink.
  • Modeling-agent system prompt has 5 NL→BOCL few-shot examples covering inv / pre / post / inheritance-touching invariant / scalar invariant. Emission is gated on the user explicitly asking — no unprompted OCL.
  • Cross-repo: requires modeling-agent c0e47e8 deployed alongside this release (./scripts/deploy.sh agent).
  • Pre-flight /validate-diagram validation is deferred to a follow-up; malformed BOCL surfaces post-hoc through the validator path landed in PR feat(wme): wire OCL invariants, preconditions, and postconditions #529.

Hardening and code quality

  • Identifier-injection hardening on BUML codegen — every identifier emit site routes through safe_var_name. Closes a code-execution vector via the exec()'d generated file.
  • De-duplicate constraint names across OCL boxes with stable insertion order (was a crash, surfaced by a 2000-constraint stress test).
  • Pre/post boxes preserve description on JSON↔BUML round-trip; auto-generated invariant names embed back into constraint.expression for stable round-trips.
  • Sorted method_contracts emission so generated Python is byte-stable.

Web Modeling Editor — Templates and Project Hub

  • Full Project tab in the template library; two new full-project bundles: Library Full Stack and Personalized Gym Agent.
  • Modeling-perspective selectable on project create.
  • Object diagram: generate objects + links from a referenced class.
  • "Full Application" preset relabelled "Full Web Application".
  • Workspace: diagram bridge wired up correctly when loading a project.
  • i18n: drop incomplete German locale.

Web Modeling Editor — Backend Fixes

  • fix(django generator) re-introduces the chdir(temp_dir) guard around django-admin startproject (regression from the router refactor).
  • fix(github deploy) surfaces real httpx.HTTPStatusError details with appropriate status mapping (401/403 pass-through, 422 → 409, other 4xx pass-through, 5xx → 502).

Cross-repo

Frontend submodule pointer moves e906308 -> 54a0741 (covers the v7.6.0 OCL UX + Full Project templates + the new add_ocl_constraint modifier). Companion frontend release PR (developmain) at BESSER-PEARL/BESSER-Web-Modeling-Editor#129.

Modeling-agent commit c0e47e8 on main adds the assistant-side NL→OCL plumbing — deploy alongside via ./scripts/deploy.sh agent.

Test plan

  • CI green (backend tests + Ruff lint + frontend lint+build).
  • python -m pytest tests/ --ignore=tests/generators/nn --ignore=tests/utilities/web_modeling_editor/backend/test_spreadsheet_import.py clean locally.
  • Open the new "Library with OCL" template — diagram loads with zero OCL warnings; «inv» / «pre» / «post» badges render.
  • Type a typo'd pre constraint — /validate-diagram reports the unknown-method error and the constraint is dropped while the 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).
  • Generate Django from a class diagram — output directory is no longer empty.
  • Deploy to GitHub with a name that already exists — UI shows the real conflict message instead of the generic 500.
  • Project hub: create a project, perspective selector controls which diagram families are visible; Full Project tab shows the two new bundles.
  • Modeling assistant: with a class diagram open, type "add a constraint that a Library always has at least one Book" — a constraint box appears linked to Library with the «inv» badge and the BOCL text.
  • Modeling assistant: NO constraint box appears unprompted (e.g., when asking the agent to "add a Book class with title and pages").
  • Full docs build: cd docs && make html — new v7.6.0 page renders, OCL parsing/AST/normalization sub-pages link.

jcabot and others added 28 commits April 29, 2026 22:31
Bundles three independently-cohesive units that benefit any future OCL
consumer (Lean encoder, SQL CHECK generators, JSON-Schema, runtime
evaluators, IDE tooling). Each unit can be split into its own PR via
interactive rebase if desired.

1. Method.pre / Method.post first-class fields
   - besser/BUML/metamodel/structural/structural.py: extend Method with
     pre and post list fields, name-uniqueness validated via property
     setters, plus add_pre / add_post helpers.
   - tests/BUML/metamodel/structural/test_structural.py: 5 new tests.
   Constraints attached to operations are now anchored on the Method;
   no naming-convention scraping required.

2. Constraint.expression typing tightened, OCLConstraint.ast added
   - structural.py: Constraint.expression is now strict str (TypeError
     on non-str); the loose Any annotation is gone.
   - besser/BUML/metamodel/ocl/ocl.py: OCLConstraint validates its
     expression argument is an OCLExpression, pretty-prints it for the
     base's source-text expression, and exposes the AST via a new ast
     property (with a setter that refreshes the source text).
   - normalization/normalize.py:61: clone(constraint.expression) ->
     clone(constraint.ast) since the AST now lives on .ast.
   - pretty_printer.py:59: same .expression -> .ast migration.
   - tests/BUML/metamodel/structural/test_structural.py: 2 new tests
     for Constraint TypeError.
   - tests/BUML/notations/ocl/test_parse_ocl.py: 5 new tests for the
     ast / expression separation.
   - tests/BUML/notations/ocl/test_normalization.py,
     test_pretty_printer.py, test_wrapping_visitor.py: 9 in-place
     migrations of constraint.expression -> constraint.ast.

3. SourceLocation tracking on OCLExpression
   - ocl.py: optional line, col, source_text fields with type-validated
     setters and a copy_location_from helper for synthetic nodes.
   - notations/ocl/visitor.py: BOCLVisitorImpl.visit() override
     populates the three fields from each ANTLR ctx's start token and
     getText().
   - metamodel/ocl/clone.py: clone() preserves location through deep
     cloning via the new helper.
   - normalization/normalize.py: _rewrite_pass calls
     copy_location_from after each rule fires so a rewritten root
     inherits its origin location.
   - tests/BUML/notations/ocl/test_parse_ocl.py: 4 new tests.
   - tests/BUML/notations/ocl/test_normalization.py: 1 new test for
     normalization preservation.

Verification: full BESSER non-generator suite at 707 passed / 5 skipped
(up from 695 before this change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DjangoGenerator.generate() shells out to `django-admin startproject` with
no cwd, and several internal paths are derived from os.getcwd(). The pre-
refactor backend.py wrapped the call in chdir(temp_dir)/try-finally; the
extracted _generate_django helper in generation_router.py dropped that
guard, so the project got scaffolded in the FastAPI process's cwd while
the harvester walked an empty temp_dir/<project_name>, producing
"Django project generation failed: Output directory is empty".

Re-introduce the chdir guard, scoped to the worker thread that runs
generate(), and restore the original cwd in finally.
deploy_webapp_to_github lets httpx.HTTPStatusError fall through to the
catch-all handler, which buries the actual cause ("name already exists
on this account", expired token, rate limit) under "An internal error
occurred during GitHub deployment." and a 500. The frontend then has
nothing useful to show in the popup.

Add a dedicated httpx.HTTPStatusError branch that pulls the message out
of GitHub's response body — preferring the nested errors[*].message
that names the offending field — and re-raises with a meaningful
status: 401 / 403 pass through, 422 maps to 409 Conflict (so the UI
can offer rename/overwrite), other 4xx pass through, and 5xx becomes
502 Bad Gateway.
OCLConstraint.__init__ and the ast setter wrapped pretty_print() in a
broad `except Exception`, falling back to `str(expression)`. The fallback
is itself broken (OCLExpression.__str__ returns None) and the broad
catch hides real pretty-printer bugs by silently writing "None" into
Constraint.expression — a defect that would surface much later as a
mysterious "None" in downstream diagnostics.

Narrow to `except ImportError` (the only documented reason for the
deferred import), use try/else to keep the import scope local, and fall
back to repr() — at least visibly typed — only when the import itself
fails. Any other exception now propagates so a real bug surfaces at the
construction site.
…on copy

_rewrite_pass guarded the copy_location_from() call with
hasattr(new_node, "copy_location_from"). Every rule.rewrite() returns
an OCLExpression (verified across rules/_helpers.py and each rule
module), so the hasattr is duck-typing where the real contract is
"new_node is an OCLExpression."

Replace with isinstance(new_node, OCLExpression). This asserts the
actual contract, matches the same check used in clone.py:30, and
crashes loudly if a future rule ever returns something unexpected
(e.g. None) instead of silently skipping the location copy.
The visitor's source-location population wrapped ctx.getText() in
`try: ... except Exception: pass`. ANTLR's ParserRuleContext.getText()
concatenates token text from the subtree and does not raise under
normal parses; the bare except was defensive against a problem that
doesn't exist and would silently hide any real failure.

Drop the try/except. If getText() ever raises, the error surfaces at
the parse site rather than being swallowed into a None source_text.
Two doc updates triggered by the iteration-1 OCL infrastructure:

- ocl_grammar.rst: the evaluation example printed
  `str(constraint.expression)` from a time when `expression` could be
  either a string or an AST. Drop the redundant `str()` (it's always
  `str` now) and add a short note clarifying that `.expression` holds
  the source text and `.ast` exposes the parsed tree on OCLConstraint.

- ocl.rst: the page declares BOCL supports preconditions and
  postconditions but never showed how they're anchored on a model. Add
  a section with a worked example using `Method.add_pre` / `add_post` —
  the new canonical wiring point that replaces the previous
  naming-convention scraping.
Regression hygiene flagged by a docs audit alongside the iteration-1
infrastructure work. Three independent issues:

- examples/ocl.rst: the constraint example `context Book: self.pages > 0`
  is not valid BOCL — the grammar requires `inv:`, `pre:`, `post:`, or
  `init:` after the context name. Add `inv:`.

- buml_language/model_building/ocl_grammar.rst: the page referenced
  `tests/ocl/test_ocl_parser.py` for grammar test cases; that file does
  not exist at that path. Point at the actual location,
  `tests/BUML/notations/ocl/test_parse_ocl.py`.

- api/BUML/metamodel/api_ocl.rst: the autodoc page only exposed
  `besser.BUML.metamodel.ocl.ocl`. Expand to also cover `clone`,
  `notations.ocl.api` (parse_ocl), `notations.ocl.error_handling`
  (BOCLSyntaxError), `notations.ocl.pretty_printer` (pretty_print),
  and `notations.ocl.normalization.normalize` (normalize) — the rest
  of the user-facing OCL surface that was unreachable through API docs.
Substantive docs pass for the iteration-1 OCL infrastructure. The
overview page (`buml_language/model_types/ocl.rst`) gains a sub-toctree
that produces a navigable tree in the sidebar; three new sub-pages
under `model_types/ocl/` cover the public surface a tool walking the
OCL AST needs:

- parsing.rst — `parse_ocl`, the .expression / .ast split,
  `BOCLSyntaxError`, source-location fields, anchoring constraints on
  `Method.pre` / `Method.post`.
- ast.rst — `OCLExpression` inheritance and the major node types
  (`OperationCallExpression`, `PropertyCallExpression`, `IfExp`,
  `LoopExp`, `IteratorExp`, literals); the `[lhs, InfixOperator, rhs]`
  shape of binary calls; the role of the wrapping visitor.
- normalization.rst — `normalize`, `pretty_print`, `clone`, including
  source-location preservation across rewrites and the caveat that
  `source_text` after rewrite describes the origin node, not the
  current shape.

`examples/ocl.rst` is rewritten end-to-end into a runnable example:
build a domain model, parse pre/post into ASTs, attach via add_pre /
add_post, inspect source location, normalize, pretty-print.
Iteration 1 included line / col / source_text fields on every
OCLExpression, populated by BOCLVisitorImpl.visit() and preserved
through clone() and normalize(). On reflection the value was thin for
BESSER specifically: OCL constraints in BESSER are embedded as string
fields inside BUML models (not edited as standalone .ocl files), so
"line N column M" rarely points anywhere a tool can usefully open. The
sub-expression highlighting use case remains real, but is better
addressed when there is a concrete consumer asking for it (so the
shape of the location data — whitespace handling, post-rewrite
semantics — can be designed against a real requirement).

Removes:
  * line / col / source_text properties + setters on OCLExpression
  * copy_location_from() helper
  * visit() override in BOCLVisitorImpl that populated the fields
  * location preservation in clone() — clone collapses back to a
    single function instead of the wrapper + _clone_inner split
  * location preservation in normalize._rewrite_pass
  * 4 tests in test_parse_ocl.py (root location, inner-node distinct
    locations, setter type validation, copy_location_from helper)
  * 1 test in test_normalization.py (location preservation across
    implies -> not-or rewrite)
  * Source-location sections in docs (parsing.rst, ast.rst,
    normalization.rst, examples/ocl.rst)

The other two iteration-1 units stay: Method.pre / Method.post
first-class fields, and the OCLConstraint .expression / .ast
separation.
feat(ocl): Method.pre/post and OCLConstraint AST/source split
…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.
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.
feat(wme): wire OCL invariants, preconditions, and postconditions
…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.
Pre/post examples in ocl.rst, parsing.rst, and examples/ocl.rst all used
``context Account inv: ...`` headers and overrode .name afterwards. That
parses (the inner expression is what add_pre/add_post stores) but it's
pedagogically wrong — readers copy the inv: header and then trip on the
WME's parse_constraint_text, which routes by the kind keyword in the
header. Switch every pre/post example to the canonical
``context Class::method(p: Type) pre|post:`` form.

Also:

- Replace the single misleading invariant example
  (``context library inv inv1: self.books>0``) with a header-shape
  reference table covering all four BOCL kinds (inv, pre, post, init)
  and one canonical example for each.
- Add an "Authoring constraints in the Web Modeling Editor" section to
  ocl.rst — the page used to be entirely Python-API-flavoured even
  though most users edit OCL through the editor's constraint box. New
  section names the «inv»/«pre»/«post» badge and the description field.
- parsing.rst: note that the auto-detect regex only handles the
  invariant header shape; pre/post/init must pass context_class
  explicitly. Same callout in ocl.rst beside the pre/post Python
  example.
- normalization.rst: tighten the termination claim — each rule
  strictly decreases the lexicographic measure, max_iterations exists
  to surface a developer bug (a rule that violates the measure)
  rather than to bound a "guaranteed" termination.
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.
@ArmenSl ArmenSl marked this pull request as ready for review May 7, 2026 08:14
@ArmenSl ArmenSl merged commit 59e6e92 into master May 7, 2026
6 checks passed
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.

From NL to OCL

3 participants