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
Open
feat(intent): a create-from on the process-step axis, and an opt-in append cardinality (#6800)#6824delchev wants to merge 1 commit into
delchev wants to merge 1 commit into
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #6800.
What was missing
"On event E, append a derived row" — a
LogEntryper process step, a protocol line per status change — had no expression. Every candidate answers a different question:postings/postsare idempotent per source,rollups/aggregatesrecompute an existing row and never insert, and an event-drivengeneratescreate-from is at-most-once by construction. What was left was a hand-written listener undercustom/, or anoutbound→inboundloopback 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
onStepReached/onStepCompleted: { process, step }, narrowed by one rule of its own: the process'strigger:entity must be thefrom: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; afromUses:source is rejected (a process and its steps belong to the model that declares them);when:stays optional — the step is the moment.mode:—once(default, unchanged) orappend, 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 underappend.Two things worth reviewing
StepEventSupport.boundEventshad to learn aboutgenerates, not just the glue builder.emitters()reads that list, so a moment whose only consumer is a create-from would otherwise get noJavaDelegateemitter 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'sdestination()silently lost its suffix on the first run here (…-Slip${topicSuffix}), caught by the existingIntentEmissionCoverageITassertion.What
appendis notThe 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
outboundalready 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 onmode: once. And twoappendrules 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 tooncepairs only. Both are stated in the template javadoc, the assistant guide and the moduleCLAUDE.md.Tests
GeneratesIntentTest— the step binding parses; unknown process / unknown step / non-eventable kind / trigger-entity mismatch / cross-model source / amodewith no trigger / an unknownmode/ a missing back-reference underappend/ a prompt on an appending create-from all rejected with named messages; the default cardinality isonce.GlueGeneratesTest— the step-scoped topic,appendModedropping 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:validateclean, engine-intent 703 + ide-template 77 unit tests green, javadoc clean under-P release.Docs
intent-assistant-guide.md+engine-intent/CLAUDE.md(this PR).0021) and docs: the create-from's step axis and its append cardinality IntentFile/intentfile.github.io#30, both left open for the maintainer.