feat(intent+templates): namespace generated handlers + bean/entity names per module#6401
Merged
Conversation
…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>
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.
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):
gen.eventsJava package. Every intent-generated event handler (trigger, rollup, notification, settlement, snapshot, generate, transition, posting, numbering, print-feeder, resolver, writer, …) lands in the single packagegen.eventswith 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.@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 first→Failed 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 undergen/events/<module>/), where<module>is the sanitized intent name (sales-invoices→sales_invoices). Producer and consumer derive the segment from the same value:engine-intent: newIntentNaming.javaModule(context)(an exact mirror of the template engine'ssanitizeJavaIdentifier) +IntentNaming.eventsPackage(context); all handler-FQN emission inBpmnIntentGenerator(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 22rename:paths gain the{{javaGenFolderName}}segment; all 22events/*.java.templatedeclarepackage 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 notgen.<module>.events: the model-to-code generation wipesgen/<module>wholesale on every per-model regeneration (generate.mjscleans the top-levelgen/subfolders a generation writes to). Glue placed insidegen/<module>/eventswould be destroyed by a model-only regeneration — the "events survive the per-model wipe" property is deliberate and documented. Keeping the module segment undergen/eventspreserves 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 byBpmnIntentGenerator.moduleScopedDelegate. Any other FQN (custom.*, an explicitgen.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
@Entitydeclares a module-qualified Hibernate entity name:template-application-rest-java:EntityController/EntityMyController/EntityPartnerController/reportFileEntitycontrollers 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 byIntentEmissionCoverageIT: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@Componenthandlers (Trigger,Abort,Wait,Notification,Job,Integration,Rollup,Expansion,Posting,SettlementOnPayment) and all@Controllerhandlers (Transition,Generate,Webhook,PrintFeeder) carry the qualified@Componentvalue — 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-keyingComponentContainer) is left as follow-up hardening.Tests
IntentCrossModuleCollisionIT(HTTP-only, no Selenide): two intent modules (collide-a/collide-b) each authoring a same-named entity (Recurrence) and a same-named reaction (thecancelRecurrencetransition) 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+IntentEmissionCoverageITupdated to the module-scoped paths/FQNs (35 + 12 sites); newIntentNamingTestcases pinjavaModuleto thesanitizeJavaIdentifiercontract.engine-intentunit suite (216 tests) green;IntentCrossModuleCollisionIT,IntentEngineIT,IntentEmissionCoverageITgreen 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 committedgen/events/*.java+.bpmnreferences 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