Skip to content

feat(intent): a create-from on the process-step axis, and an opt-in append cardinality (#6800) - #6824

Merged
delchev merged 2 commits into
masterfrom
feat/generates-step-append
Aug 19, 2026
Merged

feat(intent): a create-from on the process-step axis, and an opt-in append cardinality (#6800)#6824
delchev merged 2 commits into
masterfrom
feat/generates-step-append

Conversation

@delchev

@delchev delchev commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Closes #6800.

What was missing

"On event E, append a derived row" — a LogEntry per process step, a protocol line per status change — had no expression. Every candidate answers a different question: postings/posts are idempotent per source, rollups/aggregates recompute an existing row and never insert, and an event-driven generates create-from is at-most-once by construction. What was left was a hand-written listener under custom/, or an outboundinbound loopback whose only purpose was to defeat that guard.

Separately, a create-from bound only to the source's own lifecycle, while notifications, integrations and departures bind to the lifecycle or a process step — so a follow-up document belonging to a moment in a flow had no trigger to hang off.

The two additions

generates:
  - name: log-activation
    from: Claim
    to: LogEntry
    event: { onStepCompleted: { process: ClaimApproval, step: activate }, mode: append }
    map:
      Claim: id                # back-reference: REQUIRED in both modes
      amount: amount
    defaults:
      step: "activate"
      date: now
  1. The step axisonStepReached / onStepCompleted: { process, step }, narrowed by one rule of its own: the process's trigger: entity must be the from: entity, because the step event is delivered as a message about the record its process runs on and that record is what the create-from reads by id. The step must be a user/service task; a fromUses: source is rejected (a process and its steps belong to the model that declares them); when: stays optional — the step is the moment.
  2. mode:once (default, unchanged) or append, which drops the existing-target lookup so every delivery creates a row.

The back-reference stays required in both cardinalities: the dedup key under once, the created row's provenance under append.

Two things worth reviewing

  • StepEventSupport.boundEvents had to learn about generates, not just the glue builder. emitters() reads that list, so a moment whose only consumer is a create-from would otherwise get no JavaDelegate emitter and the listener would bind a topic nothing publishes to.
  • GlueGenerator (ide-template) binds an explicit key allow-list into the Velocity context. An unbound key renders as its own literal — which is exactly how the listener's destination() silently lost its suffix on the first run here (…-Slip${topicSuffix}), caught by the existing IntentEmissionCoverageIT assertion.

What append is not

The absence of a guard, not a state-aware one. Delivery is at-least-once (the step topic is published after commit, not transactionally with the step — the contract outbound already states), so a redelivery appends a duplicate row. It is not the answer to "I voided the document and cannot regenerate it" — that needs #6814's stage-aware predicate on mode: once. And two append rules sharing a target and a back-reference are legal by design (each records a different moment), which is why #6813's parse-time collision diagnostic must be scoped to once pairs only. Both are stated in the template javadoc, the assistant guide and the module CLAUDE.md.

Tests

  • GeneratesIntentTest — the step binding parses; unknown process / unknown step / non-eventable kind / trigger-entity mismatch / cross-model source / a mode with no trigger / an unknown mode / a missing back-reference under append / a prompt on an appending create-from all rejected with named messages; the default cardinality is once.
  • GlueGeneratesTest — the step-scoped topic, appendMode dropping the lookup, the emitter for a generates-only moment, and both lifecycle axes plus the click-only form unchanged.
  • IntentEmissionCoverageIT — emission tokens, and the runtime: one shipment whose all-serviceTask flow appends two log rows from two moments sharing the same back-reference, a click on the kept button appending a third, and the at-most-once sibling on the same moment minting exactly one summary that a later click hands back. Passes headless (-Dit.test=IntentEmissionCoverageIT).
  • mvn formatter:validate clean, engine-intent 703 + ide-template 77 unit tests green, javadoc clean under -P release.

Docs

delchev and others added 2 commits August 18, 2026 16:57
…rdinality (#6800)

"On event E, append a derived row" - a log entry per process step, a protocol
line per status change - had no expression: postings/posts are idempotent per
source, rollups/aggregates recompute an existing row and never insert, and an
event-driven create-from is at-most-once by construction. The shape needed a
hand-written listener or an outbound -> inbound loopback whose only purpose was
to defeat that guard.

Two additions to the generates event map close it:

- the process-step axis (onStepReached / onStepCompleted: { process, step }) that
  notifications, integrations and outbound departures already bind to, narrowed
  by one rule of its own - the process's trigger entity must be the from: entity,
  since the step event is about the record its process runs on and that record is
  what the create-from reads by id. The source must be local: a process and its
  steps belong to the model that declares them. when: stays optional - the step
  is the moment;
- mode: once (default, unchanged) or append, which drops the existing-target
  lookup in Generate.java.template so every delivery creates a row.

The back-reference stays required in both cardinalities - the dedup key under
once, the created row's provenance under append.

StepEventSupport.boundEvents had to learn about generates, not just the glue
builder: emitters() reads that list, so a moment whose only consumer is a
create-from would otherwise get no JavaDelegate emitter and the listener would
bind a topic nothing published to. GlueGenerator (ide-template) binds the five
new keys into the Velocity context - an unbound key renders as its own literal,
which is how the listener's destination silently loses its suffix.

append is the absence of a guard, not a state-aware one: delivery is
at-least-once, so a redelivery appends a duplicate, and it is not the answer to
"the target was voided, make another" (#6814). Two append rules sharing a target
and a back-reference are legal by design, which is why #6813's collision
diagnostic must be scoped to once pairs.
… its topic

The step axis (#6800) moved the event topic's suffix out of the GenerateOnEvent
template - which derived it from `isCreate` - and into the glue, where the intent
generator states it once for every axis. A `.glue` written before that carries no
suffix at all, and Velocity renders an absent reference as its own literal, so the
listener bound `...-SalesOrder${topicSuffix}` - a topic nothing publishes on, and
a create-from that silently never fires.

The binder now defaults the missing key to the lifecycle suffix that shape implied
(none for a create, -transitioned otherwise), so the template keeps emitting the
glue's value verbatim and there is still one writer of the topic. The generation
fixture keeps its pre-step entry as the regression guard and gains a step-bound,
append-mode one, which covers the branches the new template grew.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@delchev
delchev merged commit 823b503 into master Aug 19, 2026
10 checks passed
@delchev
delchev deleted the feat/generates-step-append branch August 19, 2026 12:47
delchev added a commit to TIVMOF/dirigible that referenced this pull request Aug 19, 2026
Three conflicts, all this branch's additions meeting master's:

- GenerateOnEvent.java.template: eclipse-dirigible#6824 split the topic bullet into a
  three-way step / create / transition branch, while this branch corrected
  the transition wording (the -transitioned channel is what the WORKFLOW
  routes, not every status write). Master's structure kept, this branch's
  wording applied to the transition branch.
- IntentEmissionCoverageIT fixture: both sides appended entities and seeds
  to the same shared model - both kept, and the entry-statuses rows keep
  master's `stage:` classification (eclipse-dirigible#6814) rather than this branch's older
  unclassified copies.
- IntentEmissionCoverageIT runtime: both sides appended an assertion method
  and its call - both kept, in the order they were written.

Co-Authored-By: Claude Opus 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.

generates.event: should support the process-step axis and an opt-in append cardinality (mode: once|append)

1 participant