Skip to content

intent: one setter step writes several members of one record in one statement - set: { slotKey: null, Status: DONE } (#8042) - #8055

Merged
iliyan-velichkov merged 2 commits into
masterfrom
issue-8042-set-many
Oct 22, 2026
Merged

iliyan-velichkov merged 2 commits into
masterfrom
issue-8042-set-many

Conversation

@iliyan-velichkov

Copy link
Copy Markdown
Contributor

Cause

A process setter (setField / setRelationField / clearField) writes one member per step, and every generated step is flowable:async: its own job, its own transaction. A change that is one fact therefore passed through visible half-written states. For example, a job released its slot while still reading RUNNING. Each column also published its own -transitioned event, and an instance that stopped between two steps kept the half-written state.

Change

A step may declare a set: map:

- { name: finish,      kind: serviceTask, args: { set: { slotKey: null, Status: DONE }, next: end } }
- { name: freeMachine, kind: serviceTask, args: { set: { machine.lastReading: null, machine.operator: null }, next: end } }
  • Keys are what a single-member setter accepts: a field or to-one relation of the trigger record, or one hop away. All keys must address one record; a mix is refused with a message naming both paths.
  • Values follow each key's single-member setter, with the same messages:
    • a literal, "{error}" or null (the clearField erasure) on a string/text field;
    • "{now}" on a timestamp/date field;
    • a seed name or id on a to-one relation, resolved by StatusSymbolResolver.
  • Exclusive with setField / clearField / setRelationField / value / delegate / call / javaHandler / notify. An empty map is refused.
  • Step kinds: allowed on a serviceTask, and on a userTask for relation keys only (the same rule setRelationField follows there).
  • One write: the generated delegate builds one LinkedHashMap in the authored order and calls updateProperties once, so the lifecycle edge, the stampOn stamps and the label judge the whole change, and one -transitioned is published.

Implementation notes:

  • SetFieldSupport.Setter now carries an ordered List<Assignment>.
    • Readers of a status write go through Setter.relationValue(...): CheckSupport, NumberingSupport, EdmIntentGenerator.writesStatus, BpmnIntentGenerator.gatesACheck.
    • Readers of raw args use SetFieldSupport.relationWrites(...): lifecycle reachability, StatusStepsSupport.
    • isSetterStep(...) replaces the three-arg checks in the BPMN generator, ServiceTaskHandlerGenerator and the abortOn then: validation.
  • GlueGenerator.setterAssignments renders each member's Java value. SetField.java.template gains an #if($assignments) branch; the single-member branches are untouched.
  • Byte-identical: a one-key set: projects exactly the single-member descriptor (same delegate), and models without set: produce unchanged descriptors and sources. No existing test was modified.
  • Trap fixed: IntentParser's Gson round trip of the YAML tree drops null map values, which would silently delete every erasure. restoreSetMaps puts each step's raw set: map back after the typed mapping.
  • Docs: the assistant guide's setter section recommends set: whenever several members change as one fact; intent-dsl-features.md and the engine-intent CLAUDE.md describe the construct.

Verification

  • Unit tests, all green, with no existing test changed:
    • engine-intent 1779 tests, including the new SetManyIntentTest (14) and 4 new GlueSettersTest cases;
    • ide-template 309 tests, including a new GlueGeneratorTest case for the rendered values.
  • Integration tests, all green:
    • new IntentSetManyIT (generate → publish → compile → run):
      • the trigger-record form clears the error and moves to RUNNING;
      • the one-hop set: { job.slotKey: null, job.Status: DONE } releases the slot, moves the status and stamps finishedAt in one write;
      • a DONE → RUNNING restart that the lifecycle refuses fails the step and leaves the slot unreclaimed.
    • IntentOneHopSetterIT, IntentClearFieldIT, IntentSetFieldNowIT, IntentEngineIT (83 tests).
  • mvn -T 1C formatter:validate with the formatter cache wiped; release-profile javadoc build on engine-intent and ide-template.
  • Not run: the full IT suite or the PostgreSQL leg; CI covers those.

Found on the way (not fixed here)

A process step named start, of any kind, collides with the BPMN start event's id start, and Flowable refuses the deployment ("multiple occurrences of ID value 'start'"). This predates the change, and the issue's own example uses that name. The IT and the guide's example name the step begin instead. It deserves its own issue: the parser could reserve the name, or the generator could prefix the start event's id.

Fixes #8042

🤖 Generated with Claude Code

…tatement - set: { slotKey: null, Status: DONE } (#8042)

A process setter wrote one member per step, and every generated step is
flowable:async - its own job and transaction - so a change that is one fact
(a job releasing its slot as it completes) passed through a half-written
state another job could observe, published one -transitioned per column,
and stayed half done when an instance stopped between the two steps.

A step may now declare a set: map (serviceTask, or relations-only on a
userTask). Each key is what a single-member setter accepts - a field or a
to-one relation, of the trigger record or one hop away - and its value
follows that setter's rule and message: a literal, "{error}" or null (the
clearField erasure) on a string/text field, "{now}" on a timestamp/date
field, a seed name or id on a to-one relation. All keys must address one
record, and set: is exclusive with every other step action.

- SetFieldSupport.Setter carries an ordered List<Assignment>; status-write
  readers ask relationValue(), raw-args readers use relationWrites(), and
  isSetterStep() is the single "is this a setter step" test.
- The generated delegate builds one LinkedHashMap and calls
  updateProperties once, so lifecycle, stampOn and label judge the whole
  change and one event is published. GlueGenerator renders each member's
  Java value; a one-key map projects the single-member descriptor, so it
  renders the existing delegate, and models without set: are unchanged.
- StatusSymbolResolver resolves a seed name on each relation key.
- IntentParser.restoreSetMaps puts each step's raw set: map back after the
  Gson round trip, which drops null map values - the erasure.

Verified: engine-intent (1779) and ide-template (309) unit suites green,
existing setter tests unchanged; IntentSetManyIT, IntentOneHopSetterIT,
IntentClearFieldIT, IntentSetFieldNowIT, IntentEngineIT green;
formatter:validate with the cache wiped; release-profile javadoc build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, not key/value varargs (#8042)

CodeQL (java/index-out-of-bounds) flagged the test helper's pairwise
flags[i + 1] read: an odd number of varargs would overrun the array.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iliyan-velichkov
iliyan-velichkov merged commit 1ecdafb into master Oct 22, 2026
16 checks passed
@iliyan-velichkov
iliyan-velichkov deleted the issue-8042-set-many branch October 22, 2026 13:27
iliyan-velichkov added a commit that referenced this pull request Oct 22, 2026
… start event's id (#8058) (#8060)

BpmnIntentGenerator gave every start event the fixed id `start`, and every
step becomes an element whose id is its name. A step named `start` therefore
produced two elements with one id: Generate succeeded, and publish failed with
`cvc-id.2: There are multiple occurrences of ID value 'start'`, leaving the
.bpmn FAILED and the process never started.

The start event's id is now chosen per process: `start` unless an element of
the process already carries it, then `startEvent` (numbered if that is taken
too). The chain, the start event, its outgoing flow and the diagram layout all
use the chosen id - nodeSize no longer draws a step named `start` as the
event's circle. Only a colliding process changes; every other generated .bpmn
stays byte-identical, and authors get no new naming rule.

Verified: StartEventIdBpmnTest (fails on master with duplicate id [start],
passes here), the engine-intent unit suite (1796 tests), and IntentEngineIT,
IntentEmissionCoverageIT and IntentSetManyIT - whose trigger step is named
`start` again (it had been renamed to `begin` to dodge this, #8055) and now
deploys and runs. formatter:validate green with the cache wiped.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

intent: one setter step writes several members of one record in one statement - set: { slotKey: null, Status: DONE }

2 participants