Skip to content

feat(engine-intent): cross-model generates SOURCE (fromUses:) - #6638

Merged
delchev merged 1 commit into
masterfrom
feat/intent-generates-cross-model-source
Aug 9, 2026
Merged

feat(engine-intent): cross-model generates SOURCE (fromUses:)#6638
delchev merged 1 commit into
masterfrom
feat/intent-generates-cross-model-source

Conversation

@delchev

@delchev delchev commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What

Adds fromUses: to a generates (create-from) block: an optional model alias for the source
entity. 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 generates
without fromUses: renders byte-identically (the new template variables default to the local values).

# authored in `delivery-notes`, whose DeliveryNote already references inventory's GoodsIssue
generates:
  - name: delivery-note-from-goods-issue
    from: GoodsIssue
    fromUses: inventory        # the SOURCE is owned by the `inventory` model
    to: DeliveryNote           # the TARGET is local
    forEntity: GoodsIssue

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 exactly
one 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 under uses:; the source, its items and its status
    relation are no longer validated against this model (they resolve from the owner's .model, exactly
    as a cross-model target already does). 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 points at the new key.
  • CrossModelSupport.TargetInfo: new statusProperty, read off the owner .model's
    DOCUMENT_STATUS widget — the same fact RelationIntent.isEntityStatus() carries locally — so the
    sourceStatus: completion hook works for a cross-model source. It is never guessed: the status
    FK is author-named (Status / Stage / State), and a wrong guess would emit updateProperty
    against a non-existent column, failing only on the button's first click.
  • GlueIntentGenerator + generateUtils.js + Generate.java.template: fromGenFolder /
    fromProjectName point the source half at the owner's gen folder and at the -transitioned topic of
    the 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-action point —
    the point that view actually reads — while the descriptor file and the controller stay in the
    authoring project.

Verification

  • Guide section documenting the keyword, when to reach for it, and its rules.
  • New parser coverage (accepts a cross-model source with no local source entity; rejects an undeclared
    alias; rejects a forEntity that is not the source) and glue coverage (owner gen folder + owning
    project resolved; a local generate keeps the markers empty).
  • engine-intent suite: 347 tests, 0 failures.
  • Exercised end to end on two real module pairs: the emitted controller reads
    gen.inventory.data.goodsissue.* / gen.opportunities.data.opportunity.*, writes the local target,
    registers on inventory-custom-action / opportunities-custom-action, and emits
    updateProperty(id, "Stage", 3) + opportunities-Opportunity-Opportunity-transitioned — the
    author-named property and the owner's project, both read from the owner model.

🤖 Generated with Claude Code

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>
@delchev
delchev merged commit 693b94a into master Aug 9, 2026
10 checks passed
@delchev
delchev deleted the feat/intent-generates-cross-model-source branch August 9, 2026 14:22
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.

1 participant