Skip to content

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

Open
delchev wants to merge 1 commit into
masterfrom
feat/generates-step-append
Open

feat(intent): a create-from on the process-step axis, and an opt-in append cardinality (#6800)#6824
delchev wants to merge 1 commit 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

…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.
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