feat(engine-intent): cross-model generates SOURCE (fromUses:) - #6638
Merged
Conversation
A `generates` create-from could only be authored on the module owning the SOURCE, with the target optionally foreign (`uses:`). `fromUses:` adds the mirror: the source is foreign and the target is local, so the same button can be authored on the module owning the TARGET. Why it matters beyond symmetry: "A generates into B" authored in A puts a reference to B's entities inside A's generated controller. When B already holds a foreign key back into A - which is usually WHY the create-from exists - the two modules' generated Java is mutually dependent and neither can be compiled, or packaged as a jar, before the other. Authoring the create-from in B with `fromUses: A` leaves exactly one edge, B -> A, and the pair is a DAG again. Same reasoning that made a schedule's source addressable across models (#6532). - GeneratesIntent: `fromUses` + isCrossModelSource(). - IntentParser: the alias must be declared under uses:; the source, its items and its status relation are no longer checked against this model (they are resolved from the owner .model, as a cross-model target already is); forEntity must BE the source - the button is contributed onto the owner's view and no local view carries the source id the endpoint needs. The unknown-local-source message now names the new key. - CrossModelSupport.TargetInfo: statusProperty, read off the owner .model's DOCUMENT_STATUS widget (the same fact isEntityStatus() carries locally), so the sourceStatus completion hook works for a cross-model source. Never guessed - a wrong guess would emit updateProperty against a column that does not exist, failing only on the button's first click. - Glue + generateUtils + Generate.java.template: fromGenFolder / fromProjectName target the source's gen folder and the "-transitioned" topic of the project that OWNS the source. Both default to the local values, so a local generate renders byte-identically. - GeneratesIntentGenerator: the button registers on the OWNER's <owner>-custom-action point - the one that view actually reads - while the descriptor file and the controller stay in the authoring project. Guide section + parser/glue coverage; engine-intent suite 347 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
What
Adds
fromUses:to agenerates(create-from) block: an optional model alias for the sourceentity. Until now the source had to be local (
from:) and only the target could be foreign (uses:);this is the mirror — a foreign source with a local target. Fully backward compatible: a
generateswithout
fromUses:renders byte-identically (the new template variables default to the local values).Why
Not symmetry for its own sake — it removes a class of unbreakable module dependency cycle.
"A generates into B" authored in A puts a reference to B's entities inside A's generated controller.
When B already holds a foreign key back into A — which is usually why the create-from exists — the
two modules' generated Java is mutually dependent. Under the runtime model that is invisible, because
every project compiles in one flat batch. The moment modules are compiled and published
independently (AOT packaging, #6400 / #6592), it is fatal: neither member of the pair can be built
first, so neither can ever be released.
Authoring the same create-from on the module that owns the target, with
fromUses: A, leaves exactlyone edge — B → A — and the pair is a DAG again. This is the same reasoning that made a schedule's
source addressable across models in #6532.
Found while unblocking AOT jar packaging for a 79-module suite: two pairs were mutually dependent, and
with this keyword the whole graph became acyclic and all 31 Java-bearing modules now compile
leaf-first against the released 14.18.0 SDK.
How
GeneratesIntent:fromUses+isCrossModelSource().IntentParser: the alias must be declared underuses:; the source, its items and its statusrelation are no longer validated against this model (they resolve from the owner's
.model, exactlyas a cross-model target already does).
forEntitymust be the source — the button is contributedonto the owner's view, and no local view carries the source id the endpoint needs. The
unknown-local-source message now points at the new key.
CrossModelSupport.TargetInfo: newstatusProperty, read off the owner.model'sDOCUMENT_STATUSwidget — the same factRelationIntent.isEntityStatus()carries locally — so thesourceStatus:completion hook works for a cross-model source. It is never guessed: the statusFK is author-named (
Status/Stage/State), and a wrong guess would emitupdatePropertyagainst a non-existent column, failing only on the button's first click.
GlueIntentGenerator+generateUtils.js+Generate.java.template:fromGenFolder/fromProjectNamepoint the source half at the owner's gen folder and at the-transitionedtopic ofthe project that owns the source, so the owner's postings and integrations observe the flip
unchanged. Both default to the local values.
GeneratesIntentGenerator: the button registers on the owner's<owner>-custom-actionpoint —the point that view actually reads — while the descriptor file and the controller stay in the
authoring project.
Verification
alias; rejects a
forEntitythat is not the source) and glue coverage (owner gen folder + owningproject resolved; a local generate keeps the markers empty).
gen.inventory.data.goodsissue.*/gen.opportunities.data.opportunity.*, writes the local target,registers on
inventory-custom-action/opportunities-custom-action, and emitsupdateProperty(id, "Stage", 3)+opportunities-Opportunity-Opportunity-transitioned— theauthor-named property and the owner's project, both read from the owner model.
🤖 Generated with Claude Code