Skip to content

feat(intent+templates): namespace generated handlers + bean/entity names per module#6401

Merged
delchev merged 1 commit into
masterfrom
feat/intent-gen-namespacing
Jul 24, 2026
Merged

feat(intent+templates): namespace generated handlers + bean/entity names per module#6401
delchev merged 1 commit into
masterfrom
feat/intent-gen-namespacing

Conversation

@delchev

@delchev delchev commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Problem

A whole-suite clean Publish-All of many intent-generated modules surfaces two pre-existing cross-module collisions between modules that author a same-named entity/reaction (observed live between two invoice modules built from the same recipe):

  • A — shared gen.events Java package. Every intent-generated event handler (trigger, rollup, notification, settlement, snapshot, generate, transition, posting, numbering, print-feeder, resolver, writer, …) lands in the single package gen.events with a class name derived from the reaction — not the module. Two modules authoring a same-named reaction collide by FQN, and since client Java compiles registry-wide in one batch, the synchronizer refuses the duplicate (Java class [gen.events.X] is already declared at […]) — which can abort the whole sync.
  • B — simple-class-name keying. The client-Java bean container keys beans by @Component("value") or else the decapitalized simple class name, and the Hibernate entity name defaults to the simple class name in one shared SessionFactory. Two modules with a same-named entity (Recurrence) collide by bean name (Duplicate client bean name [recurrenceController] …; keeping the firstFailed to register controller […]) and by entity name — regardless of package, so Fix A alone does not fix B.

The platform "keeps the first", so one module's feature silently fails to register.

Fix A — gen.events.*gen.events.<module>.*

Generated event handlers now land in the module-scoped package gen.events.<module> (files under gen/events/<module>/), where <module> is the sanitized intent name (sales-invoicessales_invoices). Producer and consumer derive the segment from the same value:

  • engine-intent: new IntentNaming.javaModule(context) (an exact mirror of the template engine's sanitizeJavaIdentifier) + IntentNaming.eventsPackage(context); all handler-FQN emission in BpmnIntentGenerator (resolvers, field/timer loaders, writers/setters, setField/setRelationField) and the transition/generate custom-action endpoint URLs (/services/java/<project>/gen/events/<module>/<Class>/run) use it.
  • template-application-events-java: all 22 rename: paths gain the {{javaGenFolderName}} segment; all 22 events/*.java.template declare package gen.events.${javaGenFolderName};.
  • template-application-ui-harmonia-java: the two print-feeder REST paths (document + manage form pages) gain the module segment.

Design note — why gen.events.<module> and not gen.<module>.events: the model-to-code generation wipes gen/<module> wholesale on every per-model regeneration (generate.mjs cleans the top-level gen/ subfolders a generation writes to). Glue placed inside gen/<module>/events would be destroyed by a model-only regeneration — the "events survive the per-model wipe" property is deliberate and documented. Keeping the module segment under gen/events preserves that property, keeps path = package, and needs no wipe-logic changes, while providing exactly the same FQN disambiguation.

Authored intents keep working unchanged: a delegate: gen.events.<ClassName> reference (no further package segment — class names cannot contain dots) means "the generated events class of THIS module" and is rewritten to the module-scoped FQN by BpmnIntentGenerator.moduleScopedDelegate. Any other FQN (custom.*, an explicit gen.events.<module>.X) passes through verbatim.

Fix B — module-qualified bean + entity names (option B1 from the analysis)

Every generated bean now carries an explicit, module-qualified name, and the generated @Entity declares a module-qualified Hibernate entity name:

  • template-application-rest-java: EntityController / EntityMyController / EntityPartnerController / reportFileEntity controllers gain @Component("<module>_<Class>") alongside @Controller; the four hand-built HQL sites (repository.query("from <Entity>Entity e …") — master-scoped list, filter search, my/partner scoped reads) now reference the qualified root entity name to match (caught by IntentEmissionCoverageIT: UnknownEntityException: Could not resolve root entity 'ClaimLineEntity').
  • template-application-dao-java: the repository gains @Component("<module>_<Entity>Repository") alongside @Repository; the entity declares @Entity(name = "<module>_<Entity>Entity") (the entity-name key in the one shared dynamic-map SessionFactory — two same-named entities across modules would otherwise be a duplicate mapping).
  • template-application-events-java: all @Component handlers (Trigger, Abort, Wait, Notification, Job, Integration, Rollup, Expansion, Posting, SettlementOnPayment) and all @Controller handlers (Transition, Generate, Webhook, PrintFeeder) carry the qualified @Component value — needed because even with Fix A, two same-named reactions in different modules would still produce the same simple-name-derived bean name.

Generated wiring is by-type (unique per generated class), so the name change is invisible to generated and hand-written code that injects by type or uses Beans.get(Class). The platform alternative (FQN-keying ComponentContainer) is left as follow-up hardening.

Tests

  • New IntentCrossModuleCollisionIT (HTTP-only, no Selenide): two intent modules (collide-a / collide-b) each authoring a same-named entity (Recurrence) and a same-named reaction (the cancelRecurrence transition) are generated, published and synchronized together, asserting the outermost layer: the module-scoped package + qualified names in the emitted sources, then over REST that both entity controllers create/read records and both generated transition endpoints flip the status. Pre-fix, this is exactly the shape that aborted the sync / dropped the second module's beans.
  • IntentEngineIT + IntentEmissionCoverageIT updated to the module-scoped paths/FQNs (35 + 12 sites); new IntentNamingTest cases pin javaModule to the sanitizeJavaIdentifier contract.
  • Full engine-intent unit suite (216 tests) green; IntentCrossModuleCollisionIT, IntentEngineIT, IntentEmissionCoverageIT green locally; javadoc (release profile) + formatter validated.

Compatibility / rollout

This is a gen/ layout change: already-generated apps keep working as-is (nothing at runtime hardcodes the old package; old committed gen/events/*.java + .bpmn references stay internally consistent), but every intent module must be re-generated to pick up the new layout — same rollout shape as previous generation-layout changes. Externally-called generated URLs (transition/generate actions, inbound webhooks, print feeders) gain the <module> path segment after regeneration.

🤖 Generated with Claude Code

…mes per module

Two pre-existing cross-module collisions surfaced by a whole-suite deploy of
modules built from the same recipe:

Fix A - the shared gen.events package: every generated event handler now lands
in gen.events.<module> (files under gen/events/<module>/), <module> being the
sanitized intent name via the new IntentNaming.javaModule/eventsPackage (exact
mirror of the template engine's sanitizeJavaIdentifier). Producer (BpmnIntentGenerator
handler FQNs, transition/generate endpoint URLs) and consumer (the events
template's renames + package lines, the Harmonia print-feeder paths) derive the
same segment. The module segment goes UNDER gen/events - not gen/<module>/events -
so glue keeps surviving the per-model gen/<module> regeneration wipe. An authored
"delegate: gen.events.<ClassName>" (no further dot) is rewritten to this module's
events package, so existing intents binding generated delegates keep working.

Fix B - simple-class-name keying: every generated bean carries a module-qualified
@component("<module>_<Class>") (rest-java controllers, dao-java repository, all
events-template handlers), the generated @entity declares name="<module>_<Entity>Entity"
(the entity-name key in the one shared dynamic-map SessionFactory), and the four
hand-built HQL sites in the rest-java controllers reference the qualified root
entity name.

New IntentCrossModuleCollisionIT deploys two modules authoring a same-named
entity AND a same-named reaction together and asserts both modules fully live
over REST; IntentEngineIT/IntentEmissionCoverageIT updated to the module-scoped
layout; IntentNamingTest pins the sanitizer contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@delchev
delchev merged commit f27aea2 into master Jul 24, 2026
10 checks passed
@delchev
delchev deleted the feat/intent-gen-namespacing branch July 24, 2026 09:43
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