Add engine-intent: source-of-truth .intent artefact above EDM/BPMN/form#6017
Open
delchev wants to merge 45 commits into
Open
Add engine-intent: source-of-truth .intent artefact above EDM/BPMN/form#6017delchev wants to merge 45 commits into
delchev wants to merge 45 commits into
Conversation
Introduces components/engine/engine-intent as a scaffolding for a new authoring layer: a single .intent YAML file at a project root drives the regeneration of every other model (EDM, DSM, BPMN, form, report, generated TS/Java) under gen/. Developers author only the intent; everything else is "gen" output owned by the regenerator. What's in: - Intent JPA artefact + repository + service (table DIRIGIBLE_INTENTS, artefact type "intent", file extension .intent) - IntentSynchronizer extends BaseSynchronizer; regeneration pass runs in finishing() so the gen/ output is on disk for the next reconciliation cycle (the orchestrator's file walk happens once per cycle). - SynchronizersOrder.INTENT = 5, ahead of every other artefact type so the regenerated files participate in the next cycle before any consumer synchronizer scans. - IntentTargetGenerator SPI + IntentRegenerationService + per-call IntentGenerationContext. Generators are Spring beans, discovered and ordered via @order. Each owns one slice (entities, processes, forms, reports, permissions, controllers) and must be idempotent + scoped to the gen/ subtree. - IntentModel POJOs covering the v1 shape: entities (+ fields, relations), processes (+ steps), forms, reports, permissions. - IntentParser uses SnakeYAML's SafeConstructor (blocks !!type / !!new tags - intent comes from LLM output and human paste, deserialisation must never be a code-execution surface) then round-trips through Gson via JsonHelper so the typed-POJO mapping lives in a single place. Wired into components/pom.xml, modules/pom.xml dependencyManagement, and group/group-engines/pom.xml. No concrete IntentTargetGenerator implementations yet - only the SPI. Concrete generators per slice, the IDE perspective (Mermaid + Claude chat + patch preview), the /custom/ escape-hatch directory, and the same-cycle-visibility orchestrator hook are all flagged as follow-ups in the module's CLAUDE.md. CLAUDE.md in the new module captures the design decisions verbatim from the design conversation: why this exists, the three things any change here must reckon with (expressiveness ceiling, LLM determinism, structured not free text), the chosen format (YAML), the open same-cycle vs next-cycle visibility question, the v1 YAML shape, and the things-not-to-do list. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tor) Closes the vertical slice for the engine-intent skeleton: - EntityIntentGenerator: first IntentTargetGenerator implementation. Writes <projectRoot>/gen/<EntityName>Entity.ts in the canonical decorator form (@entity / @table / @id / @generated / @column, @onetomany / @manytoone) so the existing EntitySynchronizer (extension Entity.ts) reconciles the regenerated files into the Hibernate dynamic-map store without further wiring. @order(100) - leaves room for the upcoming slice generators (schema 200, process 300, ...). Idempotent by construction; no timestamps in the output. - IntentEndpoint at /services/ide/intent/*: GET /projects list projects with an intent GET /projects/{project} parsed IntentModel JSON GET /projects/{project}/source raw YAML POST /projects/{project}/regenerate force a regen pass ADMINISTRATOR / DEVELOPER / OPERATOR. Constructor-injected, package conventions match engine-listeners + ide-messaging-monitoring. - components/ui/perspective-intent: perspective shell (id=intent, order=1020, three-node graph SVG icon themed via currentColor). Default region 'center' with a single view, intent-mermaid. - components/ui/view-intent-mermaid: read-only Mermaid ER renderer backed by IntentEndpoint. Project picker (bk-select), reload, regenerate, source/diagram toggle. Loads mermaid@11 from cdn.jsdelivr.net (matches the unicons CDN pattern in the rest of the IDE). Server returns parsed IntentModel; client converts to an erDiagram spec (PK marker on primary-key fields, cardinality glyphs per relation kind). mermaid.initialize uses securityLevel: 'strict'. - Registered in components/pom.xml (reactor + dependencyManagement) and components/group/group-ide/pom.xml. - CLAUDE.md updated: notes EntityIntentGenerator as the worked example, documents the perspective + view layout, trims the follow-up list to what is genuinely still pending (Claude bridge, remaining slice generators, /custom/ escape-hatch, read-only gen/ Monaco model, reverse-engineer, same-cycle visibility, schema validation). Build verified: quick-build install + formatter:validate + release-profile javadoc gate all clean for the touched modules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Every concrete ArtefactRepository must override ArtefactRepository.setRunningToAll with an explicit @query, otherwise Spring Data tries to derive a query from the method name and the context fails to start with: No property 'setRunningToAll' found for type 'Intent' Every other artefact repository (Listener, Table, View, Schema, Entity, Csvim, DataSource, OpenAPI, Camel, ExtensionPoint, Extension, ...) carries the same override. The skeleton missed it because IntentRepository was modelled on the NoRepositoryBean SPI alone, not on a working repo. Same shape as ListenerRepository. This unblocks the integration-test jobs that were failing in the open PR (#6017) - they all blew up at Spring context bootstrap, not in actual test code. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the wrong-direction EntityIntentGenerator with the correct
EdmIntentGenerator and locks in the corrected architecture in the
module CLAUDE.md so future sessions don't repeat the mistake.
The contract:
app.intent (YAML)
-> Intent generators (this engine)
gen/<intent>.edm + gen/<intent>.model (entities)
gen/<process>.bpmn (processes - TODO)
gen/<form>.form (forms - TODO)
gen/<report>.report (reports - TODO)
gen/<intent>.roles + gen/<intent>.access (permissions - TODO)
-> Existing Dirigible template engine
TS / HTML / Java / SQL under gen/<entity>/...
Intent generators stop at the model layer. They do NOT emit Entity.ts,
Controller.ts, *.java or any other code-shaped output - that artefact
belongs to the existing "Generate from EDM/Schema/BPMN" template flow.
Changes:
- Remove EntityIntentGenerator.java (was emitting decorator TS at the
wrong altitude; committed in 9570405, now reverted).
- Add EdmIntentGenerator (@order 200) writing gen/<intent>.edm (XML)
and gen/<intent>.model (JSON twin) from the entities + relations in
the intent. Both files come from a single typed map so they can never
drift. Conservative defaults for icons, menu keys, layout type,
perspective metadata, widget types - derived from the entity / field
name so the produced .edm is openable and editable as-is.
- Add IntentGenerationContext.getProjectName() so generators can derive
project-scoped paths without duplicating the parsing logic.
- Add IntentParser structural validation: duplicate names, dangling
relation / form-entity / report-source targets, unknown field /
relation / step kinds, multiple primary keys per entity. All issues
surface in one IntentValidationException with the complete list.
- Rewrite CLAUDE.md:
* New "Two-stage architecture" section at the top
* New "Wrong turns we already made" section documenting the
EntityIntentGenerator misstep and the related PermissionIntent
one so they aren't repeated
* "Concrete agreements": new top bullet restricting output
extensions to the model layer only
* "Things to not do": new bullets banning code-shaped output and
template-engine path references
* Layout / generator table / follow-ups all aligned with the new
altitude
quick-build + formatter:validate + release-profile javadoc gate all
clean on the touched modules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Second concrete intent generator (after EdmIntentGenerator). For every
ProcessIntent in the intent, writes one BPMN 2.0 file under gen/ in the
Flowable flavour the existing BpmnSynchronizer consumes.
What it emits:
- One <startEvent>, one <endEvent>, one <process isExecutable="true">.
- userTask -> <userTask flowable:candidateGroups="..." flowable:formKey="..."/>
- serviceTask -> <serviceTask flowable:async="true" flowable:delegateExpression="${JSTask}">
- script -> same shape as serviceTask
- decision -> <exclusiveGateway> with default="flow_<id>_default", plus
a conditioned outgoing flow to args.then carrying
<conditionExpression><![CDATA[${args.if}]]></conditionExpression>
- end -> the canonical <endEvent>; the explicit step's outgoing
flow targets the single shared end event
Sequence flows are emitted linearly between consecutive effective step
IDs (start -> step1 -> ... -> end), with consecutive end-event entries
collapsed so an author-declared `end` step doesn't double-emit.
Path-free references per the CLAUDE.md "no template-engine paths" rule:
flowable:formKey is the bare form name from args.form (a deployment-time
form-key resolver maps it to a generated page), and args.call for
service tasks is passed through verbatim - it should reference a hand-
authored handler under custom/, never a template output.
No BPMN diagram block (bpmndi). Flowable executes without it; the BPMN
editor in the IDE auto-lays out a missing diagram on first open.
Skipping it keeps the generator deterministic and avoids spurious x/y
churn between regenerations.
@order(300), slotted between EdmIntentGenerator (200) and the future
form (400) / report (500) / permissions (600) generators.
CLAUDE.md updated: the generator-table row for processes is now done,
both worked examples are mentioned in the layout block, and the follow-
ups list shrinks accordingly.
quick-build + formatter:validate + release-profile javadoc gate all
clean on the touched modules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
FormIntentGenerator (@order 400) writes one gen/<form>.form per FormIntent. Output is the JSON shape consumed by the form-builder editor in the IDE - metadata, feeds, scripts, code, plus a form array containing: - a header control with the form name (or description) - one input control per declared field, typed by looking up the field on the bound forEntity: string / uuid -> input-textfield text -> input-textarea integer / long / decimal / double -> input-number boolean -> input-checkbox date -> input-date timestamp -> input-datetime-local - a trailing container-hbox with one button per declared action; button colour is inferred from the action name (approve -> positive, reject/decline/delete/cancel -> negative, save/submit -> emphasized) - a code block with on<Action>Clicked stubs (TODOs); wiring to a backend is the downstream template engine's or a /custom/ override's job Path-free per the CLAUDE.md rule: no template-output URLs in the generated form. Field labels are humanized (orderDate -> "Order Date", from_date -> "From Date"). IntentEngineIT (new) is the worked end-to-end test: - HTTP-only (extends IntegrationTest, no Selenide) - One comprehensive Orders app.intent declares four entities (Customer/Product/Order/OrderItem) with relations in both directions, an OrderApproval process with every step kind (userTask + decision + serviceTask + end), two forms bound to entities (different action sets), one report and three permission roles - Writes the intent through IRepository, forces sync, then asserts: * the Intent JPA artefact is persisted (IntentService) * gen/orders.edm + gen/orders.model exist with every entity name, the right widget types per field type (NUMBER for decimal, DATE for date, CHECKBOX for boolean, TEXTAREA for text), the PRIMARY/DEPENDENT split (Order and OrderItem become DEPENDENT via incoming manyToOne edges), and all three referenced relation targets (Customer / Order / Product) * gen/OrderApproval.bpmn carries startEvent/endEvent, the userTask with candidateGroups + bare-name formKey, the exclusiveGateway, the serviceTask with delegateExpression=${JSTask}, the handler reference and the ${amount > 10000} condition expression * gen/ApproveOrder.form / gen/NewCustomer.form have the right typed controls per field, humanized labels, action buttons in the right colours, and the on<Action>Clicked stubs in the code * GET/POST /services/ide/intent/* endpoints list the project, return the parsed model with the correct sizes (4 entities, 5 process steps, 2 forms, 1 report, 3 permissions), echo the raw YAML source and trigger an explicit regenerate - A second test verifies that removing the .intent file cleans the persisted artefact CLAUDE.md updated: generator table marks .form done, layout block shows the three concrete generators, follow-ups shrinks to .report and .roles/.access. quick-build install, formatter:validate, release-profile javadoc gate, and a clean compile of tests-integrations all pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ReportIntentGenerator (@order 500) writes one gen/<report>.report per ReportIntent. The output is the JSON shape the report editor consumes: outer record with alias / tId / label / baseTable / query plus a columns array. Dimensions become columns with aggregate NONE; measures are parsed by the aggregate(field) convention (count(*) / sum(field) / avg(field) / min(field) / max(field)) into columns with the matching aggregate. Unknown shapes fall back to NONE-aggregate columns carrying the raw text as their name so the editor still loads the file. baseTable is the upper-snake of the source entity name so the .report lines up with the .edm's dataName. query / joins / filters / orders are left empty; the report editor builds the SQL on open from baseTable + columns. PermissionIntentGenerator (@order 600) writes gen/<intent>.roles from the intent's permissions block, deduped by role name with descriptions carried through. It deliberately does NOT emit .access constraints - URL-shaped access rules belong to whichever downstream template materializes the UI for an entity / form / report, because only that template knows the paths it will publish. The can: [Resource:action] tokens on each permission stay as an authoring hint to those downstream generators; the actual <path, method, role> mapping is the downstream template's contract, not intent's. Follow-ups list this trade-off. IntentEngineIT extended: asserts gen/OrdersByCustomer.report carries the intent's alias, the upper-snake baseTable, NONE-aggregate dimensions (customer.country preserved verbatim as a dotted path), and parsed COUNT/SUM aggregates from the measure expressions. Asserts gen/orders.roles contains the Sales/Manager/Administrator role entries with descriptions. CLAUDE.md updated: now lists five concrete generators; the generator table marks every defined intent block as done; layout block shows the new packages; follow-ups shrinks to the .access-from-intent question (documented as deferred) and the lower-priority extensions (DSM/CSVIM/ schema-level entries). quick-build install, formatter:validate, release-profile javadoc gate and a clean compile of tests-integrations all pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Country becomes a first-class entity in the IT YAML (id/name/code2/code3/ numeric, shape borrowed from codbex/codbex-countries) and Customer's country drops from a free-text string field to a manyToOne reference relation - matching how partner-style profiles model country lookups in codbex/codbex-partners. The EDM generator already supports manyToOne relations, so the change is purely intent-side; the .edm now carries the new Customer -> Country reference + a CUSTOMER_COUNTRY FK column on Customer. SeedIntent + IntentModel.seeds[] new POJO + collection. Each seed declares a target entity and a list of rows (key/value maps keyed by the intent's field names). Validation rejects unnamed seeds, duplicate seed names, seeds with no entity, seeds targeting unknown entities, and seeds with no rows; surfaces every problem in one IntentValidationException message. CsvimIntentGenerator (@order 700) writes two files per seed: - gen/<seed>.csvim - JSON CSVIM declaration the platform's CsvimSynchronizer consumes. Defaults match the existing platform IT fixtures (header:true, useHeaderNames:true, delimField:",", delimEnclosing:"\"", distinguishEmptyFromNull:true, version:"1.0", schema PUBLIC when the seed doesn't override it). - gen/<seed>.csv - CSV body. Header carries the entity's dataName columns (upper-snake of the field names, prefixed with the entity's dataName, e.g. COUNTRY_ID, COUNTRY_NAME). Row order matches the entity's declared field order so a row author can omit fields without misaligning the columns. Cells containing the delimiter, the quote character, or a newline are quoted and inner quotes doubled. IT YAML expanded to include the Country entity + the Customer -> Country manyToOne + a seeds block shaped after codbex/codbex-countries-data preloading three rows (Afghanistan/Albania/Algeria) into COUNTRY. The test now asserts: - the EDM declares Country and carries the new referenced="Country" link plus the CUSTOMER_COUNTRY FK column on Customer - the parsed-intent REST response carries five entities (Country included), one seeds entry, and three rows on it - gen/countries.csvim declares the COUNTRY table, PUBLIC schema, sibling CSV reference, and the platform-standard CSVIM defaults - gen/countries.csv starts with the expected COUNTRY_ID,COUNTRY_NAME, COUNTRY_CODE2,COUNTRY_CODE3,COUNTRY_NUMERIC header and carries each of the three rows CLAUDE.md updated: layout block shows the new SeedIntent POJO and the csvim/ generator package; the generator table now lists six (.csvim + .csv) as done; the YAML shape sample carries a seeds block; the done list calls out the codbex-countries / codbex-partners / codbex-countries-data inspiration that drove the IT shape. quick-build install, formatter:validate, release-profile javadoc gate and a clean compile of tests-integrations all pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
processSynchronizers() silently skips when the runtime is not prepared yet or another synchronization (e.g. the scheduled SynchronizationJob) is mid-run. Callers of the force variant - tests and the IDE publish flow - rely on the registry being reconciled when it returns, so the silent skip was a race: LocalNativeAppLifecycleIT and IntentEngineIT intermittently queried artefacts a skipped pass never persisted. The force variant now retries (100ms steps, bounded at 5 minutes) until the force flag has been consumed by a completed pass and no concurrent run is still in progress. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…form conventions The review of the scaffold found the committed IntentEngineIT could not pass; running it locally confirmed and surfaced three pipeline-killing bugs plus a set of wrong assumptions. All fixed and the IT (now three tests) passes locally: - Registry prefix: artefact locations are registry-relative but IRepository paths are repository-absolute; generated output landed outside /registry/public where neither the IT nor any downstream synchronizer could see it. resolveProjectRoot now prepends IRepositoryStructure.PATH_REGISTRY_PUBLIC. - Empty-model parse: IntentParser mapped the YAML through JsonHelper, whose Gson is configured with excludeFieldsWithoutExposeAnnotation(); every un-annotated POJO field came back null and all six generators silently skipped. The parser now uses a plain Gson with LONG_OR_DOUBLE numbers (seed id: 1 stays "1" in CSV, not "1.0"). - Output location: model files are written at the project root next to app.intent (the layout of real-world codbex application projects and every platform fixture, and the only one the model-to-code template flow is proven to handle) - never under gen/, which the templates wipe on every regeneration. Writes go through the single writeModelFile surface; a post-pass scrub removes model files no longer backed by the intent and cleans them up when the .intent itself is deleted. - Naming: the YAML name: field drives output base names and the physical table prefix (<INTENT>_<ENTITY>, e.g. ORDERS_ORDER) shared via IntentNaming across .edm dataName, .report baseTable and .csvim table - avoiding SQL reserved words and cross-project collisions. - CSVIM file paths are project-qualified (/orders/countries.csv) as CsvimProcessor resolves them against /registry/public. - EDM fidelity: required to-one relations are compositions (DEPENDENT + inherited transitive perspective, relationship* attributes on the FK property), optional ones plain associations; dropdown key/value and referencedProperty derive from the target entity's actual PK and name-like fields; the .model JSON carries perspectives/navigations and no relations array, matching editor-written documents. - Decision steps support else (gateway-default flow target) so the conditioned branch is actually skippable; then/else targets are validated at parse time; declared-but-unconsumed triggers log a warning. - INTENT_CONTENT uses the portable TEXT column definition (CLOB is not valid on PostgreSQL); mermaid is bundled as the org.webjars.npm webjar instead of loading from a CDN. CLAUDE.md is updated to match reality: the corrected path conventions (wrong turn #3), the Gson pitfall, the scrub ownership contract, the naming and decision semantics, the reference project layout, and the .gen descriptor as the future hook for programmatic model-to-code generation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
88247b1 to
ef8aeee
Compare
Adds the engine-intent section (pointer to the module guide, scope, PR reference) plus the general-purpose gotchas this work surfaced: registry-relative locations vs repository-absolute IRepository paths, the JsonHelper @Expose/pretty-print pitfalls, the now-reliable forceProcessSynchronizers semantics, stale H2 state after killed IT runs, and the bidirectional merge-order rule for dirigiblelabs sample repos (including the re-run-vs-update-branch distinction). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| p.put("dataName", column); | ||
| p.put("dataType", fkType); | ||
| p.put("dataNullable", relation.isRequired() ? "false" : "true"); | ||
| if ("VARCHAR".equals(fkType) && targetPk != null) { |
…act, not a runtime one The platform draws a sharp line this feature initially ignored: authoring artifacts (.edm, .model, .form, .report) get workspace editors plus an explicit Generate step; only runtime artifacts (.roles, .bpmn, .csvim, jobs, ...) are reconciled from the registry by synchronizers. The .edm is the precedent - it has no synchronizer, and neither should the intent. The synchronizer-based first incarnation generated into the registry, where no Projects view, modeler, or "Generate from EDM" flow could use the output, and its UI could only be a registry-reading perspective. Reworked to the editor-first developer flow: 1. create a project in your workspace 2. create app.intent (any *.intent) at the project root 3. double-click opens the new Intent Editor (components/ui/ editor-intent, registered for application/yaml+intent via the platform-editors extension point): editable YAML left, live read-only diagram right (Mermaid ER + one flowchart per process + forms/reports/roles/seeds summaries), validation issues inline, Save with dirty tracking and ctrl+s 4. Generate writes the six model files NEXT TO app.intent IN THE WORKSPACE PROJECT and refreshes the project tree - nothing is published; stale files from removed slices are scrubbed 5. publish ships intent + models together; the per-artefact synchronizers take it from there as for any project Backend: IntentSynchronizer, the Intent JPA artefact (DIRIGIBLE_INTENTS), its repository/service, and the SynchronizersOrder.INTENT constant are removed. IntentGenerationService (ex IntentRegenerationService) runs the unchanged generators against a workspace project and returns written/scrubbed. New endpoints: POST /services/ide/intent/parse (model JSON or 422 with the full issue list - feeds the editor's live diagram) and POST /services/ide/intent/generate?workspace=&project=&path= (resolved via WorkspaceService, inherently user-scoped). The .intent extension is mapped in ContentTypeHelper. The Mermaid perspective (perspective-intent, view-intent-mermaid) is deleted; its rendering moved into the editor. IntentEngineIT is rewritten against the editor services - parse, all-issues-at-once validation, workspace generation with content assertions per artefact, scrub, and 422 on invalid input. No synchronization cycles: the class runs in ~50s instead of ~6 minutes. Verified in a running instance: editor page, mermaid webjar, parse (200/422), generate into /users/admin/workspace/<project> with correct files, scrub on regenerate, registry untouched until publish. Both CLAUDE.md files document the authoring-vs-runtime artifact principle, the removed synchronizer as wrong turn #4, and the .gen descriptor chaining (GenerateService.generateFromModel, the form-builder's Regenerate mechanism) as the follow-up for one-click model-to-code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| return ResponseEntity.unprocessableEntity() | ||
| .body(Map.of("issues", e.getIssues())); | ||
| } catch (RuntimeException e) { | ||
| LOGGER.error("Intent generation failed for [{}/{}/{}]", workspace, project, path, e); |
Comment on lines
+84
to
+85
| LOGGER.info("Generating model files for intent [{}] under [{}] via {} generator(s)", IntentNaming.baseName(context), projectRoot, | ||
| generators.size()); |
| try { | ||
| generator.generate(context); | ||
| } catch (RuntimeException e) { | ||
| LOGGER.error("Intent generator [{}] failed for project [{}]", generator.name(), projectName, e); |
| try { | ||
| repository.removeResource(projectRoot + "/" + fileName); | ||
| scrubbed.add(fileName); | ||
| LOGGER.info("Scrubbed stale intent output [{}/{}]", projectRoot, fileName); |
| scrubbed.add(fileName); | ||
| LOGGER.info("Scrubbed stale intent output [{}/{}]", projectRoot, fileName); | ||
| } catch (RuntimeException e) { | ||
| LOGGER.error("Failed to scrub stale intent output [{}/{}]", projectRoot, fileName, e); |
| String fileName = form.getName() + ".form"; | ||
| if (!seenFiles.add(fileName)) { | ||
| LOGGER.warn("Duplicate form [{}] in intent [{}] - keeping the first occurrence", form.getName(), | ||
| IntentNaming.baseName(context)); |
| for (ReportIntent report : model.getReports()) { | ||
| if (report.getName() == null || report.getName() | ||
| .isBlank()) { | ||
| LOGGER.warn("Skipping unnamed report in intent [{}]", IntentNaming.baseName(context)); |
| IntentModel model = IntentParser.parse(yaml); | ||
| return ResponseEntity.ok(model); | ||
| } catch (IntentValidationException e) { | ||
| return ResponseEntity.unprocessableEntity() |
| return ResponseEntity.ok( | ||
| Map.of("workspace", workspace, "project", project, "written", result.written(), "scrubbed", result.scrubbed())); | ||
| } catch (IntentValidationException e) { | ||
| return ResponseEntity.unprocessableEntity() |
Double-clicking a .intent file routed to the editor (content type maps correctly) but the page failed to bootstrap with two errors: - view.js "You must provide one of the following: ... editorData": the page never loaded its own configs/intent-editor.js, so the platform view framework had no editorData to read. - intentEditor $injector:modulerr: the page requested only the ng-view platform-links category, so WorkspaceService / ViewParameters / the workspace hubs (provided by ng-editor) were absent and the Angular module could not satisfy its dependencies. Both are required of every workspace editor; the editor.html now loads configs/intent-editor.js in the head and requests "ng-view,ng-editor", matching the csvim/form editors. Verified against a running instance: the injected HTML now pulls intent-editor.js, view.js, workspace.js and workspace-hub.js. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The HTTP-only IntentEngineIT covers /parse and /generate exhaustively but structurally cannot catch the editor page failing to bootstrap in a browser - which is exactly the regression that slipped through (the editor.html missing the ng-editor platform-links category and its config script left the services green while the page died with $injector:modulerr). This Selenide test (modeled on BpmnEditorLoadsIT, which exists for the same reason) imports a project with an app.intent, opens it, and asserts the editor tab appears, the intentEditor AngularJS module's injector resolves (directly catching the modulerr), the source textarea and the live diagram SVG render inside the iframe, and clicking Generate writes the model files into the workspace project. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nder
The generated .edm and .bpmn opened to an empty canvas. Root cause: both
visual modelers render the diagram ONLY from a layout block - the EDM
editor decodes the .edm's <mxGraphModel> (editor-entity/js/editor.js:
codec.decode(... getElementsByTagName('mxGraphModel')[0] ...)), and the
Flowable/Oryx modeler decodes the .bpmn's <bpmndi:BPMNDiagram>. The
generators emitted only the logical model and omitted both blocks, on the
mistaken assumption that the editors auto-lay-out on open. They do not.
EdmIntentGenerator now emits an <mxGraphModel> with a deterministic grid
layout: a style="entity" vertex per entity carrying an <Entity> value, a
child vertex per property carrying a <Property> value, and an edge per
foreign-key relation wiring the owner's FK property to the target entity's
primary-key property.
BpmnIntentGenerator now emits the omgdc/omgdi/bpmndi namespaces and a
<bpmndi:BPMNDiagram> with a BPMNShape per node (sized by kind) and a
BPMNEdge per sequence flow, laid out left-to-right on a fixed lane. The
sequence-flow emission was refactored to a flow list so the elements and
their edges derive from one source.
Both layouts are deterministic (byte-stable across regenerations); the
modelers re-route on first manual edit.
Tests:
- IntentEngineIT asserts the diagram blocks, a shape/vertex, and an edge
are present in both the .edm and .bpmn.
- IntentEditorLoadsIT now opens the generated library.edm and
LoanApproval.bpmn and asserts the entity box ("Book") and the process
node ("librarianReview") actually render - directly covering the
empty-canvas regression.
CLAUDE.md and the generator javadocs are corrected: the "no diagram block,
the editor auto-lays-out" claim was wrong; the new rule is that any
generator whose target opens in a visual modeler must emit a deterministic
layout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Kit theme EDM editor: entity/property label text was invisible on the light theme (light text on the light entity background). The graph font/stroke colors came from --font_color, which styles.css switched via @media (prefers-color-scheme: dark) - the OS setting, not the IDE theme switcher. With the OS in dark mode but the IDE on the light theme, the text stayed light. --font_color/--stroke_color now follow the BlimpKit --foreground token (which the switcher sets, and which the editor body and icons already use), and the OS media override is removed. Both themes are now correct and track the switcher. Intent editor: Mermaid was initialized with the hardcoded 'default' theme, so the diagram ignored the IDE theme. It now uses theme: 'base' with themeVariables read at runtime from the live BlimpKit tokens (--sapBackgroundColor / --sapTextColor / --sapList_Background / --sapList_BorderColor, with --foreground/--background fallbacks), and re-applies + re-renders on ThemingHub.onThemeChange. The diagram now matches light/dark and follows the switch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first theming pass mapped lineColor to a divider token (--sapList_BorderColor) and node fill to a surface token (--sapList_Background); both are deliberately low-contrast, so edges and borders were nearly invisible on light and unreadable on dark. It also read raw custom-property values, which do not resolve nested var()/named colors. Now colors are resolved to concrete rgb via a probe element, and the palette is anchored on the two values that guarantee contrast: the theme foreground and background. Lines, borders and all text use the foreground (always contrasts with the canvas in both themes); node/entity fills are the foreground blended into the background at ~10% so panels read as distinct without washing out their text. Re-applied and re-rendered on theme change as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heme switch Two issues remained after the previous theming pass: - Connector lines (ER relationships, BPMN/flowchart edges) were invisible in dark mode. themeVariables.lineColor does not reach those strokes - mermaid derives the relationship/edge/arrowhead styling from its own injected CSS. Added a themeCSS block to mermaid.initialize that forces edge/relationship strokes, arrowheads, node/entity borders and label text to the theme foreground, so connectors are visible in both themes. - Switching the theme produced "Syntax error in text" diagrams. render() drove all sections through Promise.all, i.e. concurrent mermaid.render calls; mermaid 11 shares global parser/DOM state and is not reentrant, so the concurrent renders corrupted each other - reliably right after the theme-change re-initialize. render() now renders sections sequentially (await), with per-section try/catch and the supersede guard preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… build gotcha Hand-off for a clean session. The editor's Mermaid diagram pane still has two unresolved defects (invisible connector lines in dark mode; "Syntax error" bombs on theme switch) after two fix attempts. engine-intent's CLAUDE.md gains an "UNRESOLVED: Intent Editor diagram theming" section recording the exact symptoms, everything already tried (theme:'base' + themeVariables, probe-resolved colors, themeCSS, sequential render), and the prioritized hypotheses to pursue next: verify the running fat jar is actually rebuilt (most likely cause of "no change"), suspect securityLevel:'strict' stripping themeCSS (try 'loose' or post-process the SVG), log the real switch-time error instead of the generic bomb, and strengthen IntentEditorLoadsIT (a "Syntax error" bomb is itself an <svg>, so the current assertion passes on a broken diagram, and it never switches theme). Both CLAUDE.md files also record the platform-wide gotcha that UI module resources are bundled into the build/application fat jar and are not hot-reloaded, so a green IT never proves the user's running jar is current. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The diagram pane's two unresolved Mermaid defects - invisible connector lines in dark mode and a "Syntax error" bomb on every light/dark theme switch - are removed by construction by switching to mxGraph 4.2.2, the engine the EDM/schema/mapping modelers already use. render() now builds one read-only mxGraph per section: an ER-style diagram for the entities (blue HTML-label cards, solid composition vs dashed association edges, mxFastOrganicLayout) and a top-down flowchart per process (slate start/end, blue user-tasks, green service tasks, amber decision rhombus, conditioned/default edges mirroring BpmnIntentGenerator, mxHierarchicalLayout). Cells use fixed brand colours that read on both the light and dark IDE themes painted on a transparent canvas, so the diagram looks identical in either theme and needs no recolour-on-theme-switch step - which is what eliminates both defects rather than patching Mermaid's theming pipeline. editor-intent now depends on resources-mxgraph (like editor-mapping) instead of the mermaid webjar; the orphaned mermaid.version is dropped. IntentEditorLoadsIT asserts the mxGraph SVG renders and the parsed Book entity label appears in the diagram (the old "any <svg> exists" check passed on a Mermaid error bomb). Both CLAUDE.md files updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Source pane: replace the plain textarea with an embedded Monaco editor (the platform's main code editor engine) with YAML highlighting, reusing the /webjars/monaco-editor webjar editor-monaco already ships - no new dependency. The Monaco theme follows the IDE light/dark theme via ThemingHub (vs-light / blimpkit-dark / classic-dark) exactly as editor-monaco does. $scope.text stays the single source the parse / save / diagram code reads, kept in sync from Monaco's change event, which drives the same dirty-tracking and debounced re-parse the textarea's ng-change used to; the instance is disposed on $destroy. Diagram layout: mxGraph layouts place cells at arbitrary (often negative) coordinates, so translate the view to seat the content at the border (fitIntoView) - previously cells fell off the left/top edge and were clipped. Lay the entities out left-to-right with mxHierarchicalLayout (the same layout the processes use) instead of mxFastOrganicLayout, which collapsed every card onto the origin because they all start at (0,0). IntentEditorLoadsIT now waits for the Monaco editor (.intent-monaco .monaco-editor); the engine-intent guide is updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three related corrections so the generated EDM matches the conventions of
the codbex apps (which are authored by the Dirigible team and are the
canonical Dirigible conventions), driven by a full-stack generation that
failed on H2:
- Integer-only primary keys. The parser rejects a non-integer PK
(integer/int/long) and the EDM generator only emits dataAutoIncrement
for integer columns. A uuid PK had produced AUTO_INCREMENT on a
VARCHAR(36) column, which H2 rejects ("Feature not supported") during
table creation.
- Composition is opt-in via `composition: true` on a manyToOne/oneToOne,
replacing the surprising "first required to-one is automatically a
composition" heuristic. `required` alone now only means a NOT NULL FK
association; composition implies NOT NULL and makes the owner DEPENDENT.
A relation's composition relationshipName is the master's detail
collection label, taken from the parent's matching oneToMany name
(Member.loans -> "Loans", like codbex's "Items").
- Property names are PascalCase (id -> Id, loanedOn -> LoanedOn, FK
member -> Member), columns stay UPPER_SNAKE, and every property carries
auditType="NONE" - matching codbex EDMs.
IntentEngineIT and the IntentEditorIT fixture updated (integer PKs,
explicit composition, PascalCase / relationshipName / auditType
assertions, plus a new parse_rejects_a_non_integer_primary_key test).
Both CLAUDE.md files document the conventions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generated full-stack UI builds a foreign-key dropdown's data-service
URL (api/<perspective>/<entity>Service.ts) and its create-detail dialog
from relationshipEntityName + relationshipEntityPerspectiveName on the FK
property. The intent EDM only set those on the <relation> element, and
the .model carries no relations array, so they were undefined - the Book
page's Genre dropdown loaded api/undefined/undefinedController.ts and
failed on refresh.
EdmIntentGenerator now writes the full relationship metadata onto every
to-one FK property, matching the codbex .model:
- relationshipType (COMPOSITION / ASSOCIATION) and relationshipCardinality
(1_n composition, n_1 manyToOne association, 1_1 oneToOne association)
- relationshipName = <owner>_<target> (the DB FK constraint name in the
schema template) - corrects the earlier "collection label" value
- relationshipEntityName / relationshipEntityPerspectiveName /
relationshipEntityPerspectiveLabel - drive the dropdown URL + dialog
Also set isRequiredProperty="true" on required fields/FKs (the REST
controller's required-value validation keys on it, so it was silently
never firing).
template-application-rest-v2: guard the max-length validation with
$property.dataLength so a CLOB/text property (no length) no longer leaks
a literal ${property.dataLength} into the generated controller; the Java
REST template already had the guard.
IntentEngineIT asserts the new relationship attributes, ASSOCIATION/n_1,
isRequiredProperty, and relationshipName=<owner>_<target>. engine-intent
CLAUDE.md documents the conventions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The report generator produced a non-codbex shape with an empty query, so
generated reports did not run. Rewrite it to the codbex .report format
(see codbex-invoices/*.report): name / alias (source entity) / table
(intent-prefixed) / columns / a fully materialised SQL query / conditions
/ security.
The report is rooted at `source`; each dimension/measure resolves to a
physical column:
- a plain field (dueOn) -> a source column;
- a relation.field path (member.name) -> an INNER JOIN to the related
entity plus a column on it (how a report shows a parent's columns);
- a bare to-one relation (book) -> the FK column;
- count(*)/sum/avg/min/max -> an aggregate column, dimensions become the
GROUP BY.
`filter` becomes the WHERE with intent field names rewritten to qualified
physical columns (dueOn <= CURRENT_DATE -> Loan.LOAN_DUE_ON <=
CURRENT_DATE); operators/literals/CURRENT_DATE pass through. security is
{generateDefaultRoles, roleRead: <project>.Report.<name>ReadOnly}. Column
names + base table mirror EdmIntentGenerator so the report never drifts.
The report JSON is serialised with HTML-escaping disabled so the SQL
keeps literal = / > / < operators (JsonHelper escapes them to =
etc. - valid JSON but unreadable and unlike the codbex files).
IntentEngineIT covers an aggregate report (GROUP BY, SUM of a qualified
column, security) and a relation.field + filter report (INNER JOIN on the
FK, qualified WHERE). engine-intent CLAUDE.md documents the format.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| String fileName = report.getName() + ".report"; | ||
| if (!seenFiles.add(fileName)) { | ||
| LOGGER.warn("Duplicate report [{}] in intent [{}] - keeping the first occurrence", report.getName(), | ||
| IntentNaming.baseName(context)); |
| } | ||
|
|
||
| /** Qualify a single filter token to a physical column when it names a field/relation.field; else leave it. */ | ||
| private static String qualifyToken(IntentModel model, EntityIntent source, String baseAlias, String token) { |
| } | ||
|
|
||
| /** Best-effort structured condition for a single binary predicate (matches what the editor shows). */ | ||
| private static List<Map<String, Object>> conditions(IntentGenerationContext context, IntentModel model, EntityIntent source, |
…t dimensions Form: the control `model` (and id) now bind to the PascalCase EDM property name (loanedOn -> LoanedOn) via IntentNaming.pascalCase, so a generated form actually reads/writes the entity field instead of a lowercase name that no property matches. Report: a bare to-one relation dimension (dimensions: [member]) now INNER JOINs the related table and shows its label (name) field instead of the raw foreign-key id - "group by member" displays the member's name, not its id (use member.id for the raw id). relation.field paths and plain fields are unchanged. IntentEngineIT asserts the form's PascalCase model and the report's bare-relation join + group-by-name. engine-intent CLAUDE.md updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the local IntentEditorIT fixture with the published dirigiblelabs/sample-intent-model sample, cloned through the IDE Git perspective (the same clone-a-real-repo pattern the SampleProjectRepositoryIT subclasses use). The IT opens the cloned app.intent, asserts the Monaco editor + mxGraph diagram render, and that Generate writes the model files - unchanged assertions, now sourced from the shared sample. The inline-YAML IntentEngineIT stays for fast, network-free coverage. The local tests-integrations/.../IntentEditorIT fixture is removed; the sample lives in https://github.com/dirigiblelabs/sample-intent-model. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Start consuming a process trigger. trigger: { onCreate: <Entity> } now:
- is validated by the parser (the target must be a declared entity);
- makes the EDM generator add a ProcessId back-reference property
(VARCHAR) to that entity, via the new TriggerSupport helper.
This is the model-layer half. The runtime auto-start - a gen/events
listener/handler that calls Process.start and writes ProcessId back, plus
the Java DAO publishing the create event - is a separate language-template
step (the BPMN keeps a plain none-start event for now; the generator logs
an info note instead of the old "not consumed" warning).
IntentEngineIT: OrderApproval gains trigger { onCreate: Order } and the
test asserts Order's EDM carries a ProcessId property, plus a new test
that a trigger to an unknown entity is rejected. engine-intent CLAUDE.md
documents the partial wiring and the remaining gen/events step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| String fileName = process.getName() + ".bpmn"; | ||
| if (!seenFiles.add(fileName)) { | ||
| LOGGER.warn("Duplicate process [{}] in intent [{}] - keeping the first occurrence", process.getName(), | ||
| IntentNaming.baseName(context)); |
Complete the runtime half of trigger: { onCreate: <Entity> } - the intent
stays models-only; the glue-code is produced by a new language template,
not the intent generators.
- New template-application-events-java template (registered at
platform-templates like the other language templates) reads the model's
triggers collection and emits one gen/events/<Process>Trigger.java per
trigger: a client-Java @Listener (TOPIC) implements MessageHandler that
loads the created entity, calls Process.start(<process>, businessKey,
<entity JSON>) and writes the started instance id back to ProcessId.
gen/events is a sibling of gen/<model>, so it survives the per-model
regeneration wipe.
- EdmIntentGenerator now emits a triggers collection in the .model
(process/entity/perspective/keyProperty) alongside the ProcessId field.
- template-application-dao-java publishes the create event from save()
(Producer.sendToTopic('${projectName}-${perspectiveName}-${name}', json))
- the topic the handler binds to; parity with the TS DAO.
- generateUtils.js gains a "triggers" collection case (its own loop, since
triggers are not entity-shaped like the hardcoded entity collections).
IntentEngineIT asserts the .model triggers collection and runs the events
template through the real generation service end to end (the template
module is a test dependency so its content loads into the test registry,
since the group-templates aggregator's transitives are not resolved here).
Docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…en/<modelFileName> Generating the events glue (gen/events) wiped the full-stack output (gen/<model>) because generate.mjs always called cleanGenFolder(genFolderName=<modelFileName>) before writing, regardless of where the selected template actually emits. A template that targets a sibling gen folder thus clobbered another template's output for the same model file. generate.mjs now derives the distinct gen/<subfolder>s from the generated files' output paths and cleans only those, so each template scrubs its own output and leaves the others intact. Standard templates (everything under gen/<modelFileName>) are unchanged. Covered by IntentEngineIT#generating_the_events_template_preserves_the_full_stack_gen_output (regression) and #events_template_generates_the_process_trigger_handler, both green. template-application-dao-java added as an explicit tests-integrations test dependency (template content is not resolved transitively via the group-templates aggregator). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ABLE) DataTypeUtils.getDatabaseTypeName maps a JDBC type code back to its name via DATABASE_TYPE_TO_DATA_TYPE, which is the supported-types set isDatabaseTypeSupported checks. CLOB (java.sql.Types 2005) and NCLOB (2011) were missing from that map even though STRING_TO_DATABASE_TYPE parses "CLOB" the other way, so CREATE TABLE with a literal CLOB column worked while TableAlterProcessor - which reads the existing column's JDBC type code and maps it back - failed with "Type [2005] not supported". This surfaced on an intent-generated app: a `text` field maps to a CLOB column (LIBRARY_BOOK.summary), and the second sync (ALTER) blew up. Both large-text JDBC types now resolve to DataType.CLOB/NCLOB. Covered by DataTypeUtilsTest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ndler import
The generated gen/events/<Process>Trigger.java imported the entity/repository from
gen.<genFolder>.data.<Perspective>.* using the raw (PascalCase) perspective, but the
DAO/entity templates write those classes under the lowercased package segment
(javaPerspectiveName = sanitizeJavaIdentifier(perspectiveName)). javac rejected it with
"package gen.library.data.Member does not exist" (the failure was masked on macOS's
case-insensitive filesystem).
The handler now imports via ${javaPerspective} (the same lowercasing) while the
@Listener topic keeps ${perspective} so it still matches the topic the DAO publishes to
(${projectName}-${perspectiveName}-${name}). generateUtils.js's triggers case computes
javaPerspective with the shared sanitizeJavaIdentifier from parameterUtils.
IntentEngineIT now asserts the lowercase import package (a case-sensitive string check,
so it catches this regardless of the host filesystem) and the lowercase DAO output path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client .java compiles as a single javac batch in the JavaSynchronizer; until now a failure was only logged and stored on the JavaFile artefact, invisible in the IDE. Now each compile failure becomes a Problems-view entry at its exact line/column, and a fix/removal clears it. - JavaSourceCompiler.BatchResult gains structured per-FQN diagnostics (severity, line, column, message) alongside the existing formatted failure string; JavaLoader.RebuildResult threads them through. New CompileDiagnostic record. - JavaSynchronizer projects the outcome onto the Problems view via ProblemsFacade (the same pattern data-csvim's CsvimUtils uses): on failure it replaces the file's "Compilation" problems with one entry per diagnostic; on success or source removal it clears them. ProblemsFacade gains a deleteProblem(location, type, category) symmetric with save(). engine-java already pulls api-platform transitively (via the Java SDK); declared directly now. No new module edge / no dependency cycle (ide-problems is not touched). - Bonus: the Problems view (view-problems) listens for platform.publisher.published and, after a publish that raises the active-problem count (server-side sync compiles client Java a moment later), brings itself to the front via LayoutHub.focusView. Reduced or unchanged counts never steal focus. Uses MessageHubApi (the published topic) since the WorkspaceHub is not in the view's ng-view bundle. Tests: JavaSourceCompilerTest asserts structured diagnostics carry the offending line/column; JavaCompilationProblemsIT (HTTP-only) writes a broken .java, asserts a Compilation problem with a line appears at /services/ide/problems, then asserts a fix and a delete each clear it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Creating an entity with a date/time field (e.g. Book.PublishedOn -> java.time.LocalDate) threw at runtime: the generated DAO published the create event with `new Gson().toJson(saved)`, and a bare Gson falls back to field reflection which java.base refuses for java.time on JDK 17+ (InaccessibleObjectException / "module java.base does not opens java.time"). The trigger handler had the same bug on the deserialize side. Added org.eclipse.dirigible.sdk.utils.Json - a small SDK helper over a Gson pre-configured with ISO-8601 adapters for LocalDate/LocalDateTime/LocalTime - and pointed both templates at it: - template-application-dao-java Repository.java.template: Producer.sendToTopic(..., Json.stringify(saved)) - template-application-events-java Trigger.java.template: Json.parse(message, <Entity>.class) Tests: JsonTest round-trips the java.time fields a bare Gson cannot serialize; IntentEngineIT now asserts the generated repository/handler use the Json helper and contain no bare `new Gson()`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first cut registered adapters only for LocalDate/LocalDateTime/LocalTime, so creating an entity with a timestamp/audit field (mapped to java.time.Instant - e.g. Member.JoinedAt) still hit "Failed making field java.time.Instant#seconds accessible". Replaced the per-type registrations with a single TypeAdapterFactory that handles any java.time.* type: serialize via toString() (ISO-8601), parse via that type's static parse(CharSequence) - the uniform contract those types share. This covers Instant, Local/Offset/Zoned date-times, Year, Duration, etc. with no further per-type maintenance; types without a string parse fall back to Gson's default handling. JsonTest now also round-trips an Instant field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion-rest (v1)
A CLOB/text property (dataTypeTypescript "string", no dataLength) leaked an unresolved
`${property.dataLength}` into the generated <Entity>Service.ts:
if (entity.Summary?.length > ${property.dataLength}) { ... }
GraalJS reads the leaked `$` as an undefined reference, so any create/update on an entity
with a text field failed with 500 `"$" is not defined`. The v2 template
(template-application-rest-v2) and the Java template (template-application-rest-java) were
already guarded with `&& $property.dataLength && $property.dataTypeJava != "time"`; v1's
entity.ts.template only checked `== "string"`. Aligned it with the same guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nant context A client-Java @Listener handler ran directly on the ActiveMQ broker thread with no tenant context, so any handler touching tenant-scoped services failed - e.g. the intent process trigger's Process.start hit "Attempting to get current tenant but it is not initialized yet" (TenantContextImpl.getCurrentTenant via BpmProviderFlowable.getTenantId). The built-in TypeScript listener (AsynchronousMessageListener) already recovers the tenant the producer stamped on the message and wraps the handler in tenantContext.execute(tenantId, ...). The Java @Listener path (ListenerClassConsumer.dispatch) skipped that step. It now does the same: read the tenant via TenantPropertyManager and execute the dispatch within it, falling back to the default tenant when a message carries none (e.g. a non-platform sender) so the handler still runs in a valid context. TenantPropertyManager is made public (+ getCurrentTenantId(Message)) so engine-java, which runs its own JMS consumers, can reuse the same tenant-from-message logic instead of duplicating the property-name contract. The platform MessageProducer already stamps the tenant on send, so events published by the generated Java DAO (on the tenant-scoped request thread) carry it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…name
A generated BPMN user task carried `flowable:candidateGroups="<assignee>"` verbatim - the
lower-camel token from the intent (e.g. "librarian"). The Process Inbox lists tasks via
`taskCandidateGroupIn(UserFacade.getUserRoles())` (TaskServiceImpl), i.e. the candidate group
must match a role name the user holds ("Librarian"). Case-sensitive Flowable matching meant the
task started but never appeared in anyone's inbox.
BpmnIntentGenerator now resolves a user task's assignee against the declared permission roles
(case-insensitive) and emits the actual role name as the candidate group; when no role is
declared it falls back to PascalCase of the assignee, matching the role-naming convention the
.roles generator uses.
IntentEngineIT: the OrderApproval managerReview assignee is now "manager" and the test asserts
the candidate group is the resolved "Manager" (and not the raw lower-case "manager").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Inbox "Open Form" navigates straight to the task's flowable:formKey (+ taskId & processInstanceId), so a bare form name resolved relative to /services/web/inbox/ and 404'd (/services/web/inbox/ApproveLoan). The formKey is now the served form-page URL the form-builder template produces: /services/web/<project>/gen/<form>/forms/<form>/index.html. This is a deliberate, documented exception to the intent layer's "no template-engine paths" rule (updated the class javadoc): a user task cannot reference its form without naming where the form is rendered, and this matches the canonical Dirigible/codbex form-key convention. The project name comes from the generation context; the form name is args.form. IntentEngineIT asserts the full formKey URL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The form's action handlers were `// TODO: wire <action>` no-ops, so clicking Approve/Reject
did nothing. FormIntentGenerator now emits a working `code` block: it reads taskId from the
form URL (the Inbox opens the form with ?taskId=&processInstanceId=) and each on<Action>Clicked
completes the current task via the platform BPM API
(POST /services/bpm/bpm-processes/tasks/<taskId> with {action:'COMPLETE', data:{action, ...model}}),
then closes the window. A form opened without a taskId reports it instead of failing silently.
Business logic beyond completing the task stays in a custom/ override.
IntentEngineIT asserts the handler calls the completion API and is no longer a TODO stub
(matching escape-free substrings, since the .form code is Gson HTML-escaped - ' -> ',
= -> = - and un-escaped by the form-builder at load time).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…omments Comment/javadoc wording only (no logic change): the generators and module guide now describe the EDM/report/PK conventions as the canonical Dirigible model conventions rather than naming specific external application repositories. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ndation) Dirigible is a vendor-agnostic Eclipse Foundation project; the intent-layer comments and test messages now describe the EDM/form-key/PK conventions as the canonical Dirigible conventions rather than naming a specific vendor's application repositories. Comment/string-only. (Left untouched: real third-party Maven coordinates like com.codbex.olingo, and the EPL copyright headers governed by licensing-header.txt - those are repo-wide governance, not this branch's concern.) Co-Authored-By: Claude Opus 4.8 <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.
Summary
Scaffolds a new authoring layer above the existing model artefacts: a single
.intentYAML file at a project root drives the regeneration of every other model (EDM, DSM, BPMN, form, report, generated TS / Java) undergen/. Developers author only the intent; everything else is "gen" output owned by the regenerator. One altitude higher than the existing pattern (where the standard models were the source and HTML / SQL / ORM mappings were the gen output).This PR is the engine skeleton only - no concrete
IntentTargetGeneratorimplementations, no IDE perspective. The design context is captured verbatim incomponents/engine/engine-intent/CLAUDE.md.What's in
IntentJPA artefact (DIRIGIBLE_INTENTS, artefact typeintent, file extension.intent) + repository + service.IntentSynchronizer extends BaseSynchronizer; regeneration pass runs infinishing().SynchronizersOrder.INTENT = 5, ahead of every other artefact, so the regeneratedgen/files participate on the next reconciliation cycle.IntentTargetGeneratorSPI +IntentRegenerationService+ per-callIntentGenerationContext. Spring beans discovered via component scan, ordered with@Order. Each generator owns one slice and must be idempotent + scoped to<projectRoot>/gen/.IntentModelPOJOs for the v1 shape: entities (+ fields, relations), processes (+ steps), forms, reports, permissions.IntentParseruses SnakeYAML'sSafeConstructor(blocks!!type/!!newtags - intent arrives from LLM output and human paste, deserialisation must never become a code-execution surface), then round-trips through Gson so the typed-POJO mapping lives in a single place.Wired into
components/pom.xml,modules/pom.xmldependencyManagement, andcomponents/group/group-engines/pom.xml.Design notes (captured in detail in the module CLAUDE.md)
/custom/escape-hatch, patch-shaped LLM edits, structured intent rather than free text).gen/infinishing()are visible only on cycle N+1. Three resolution options are documented; the scaffold picks the do-nothing one for now.Follow-ups (not in this PR)
IntentTargetGeneratorimplementations per slice (entities, processes, forms, reports, permissions, controllers)./custom/escape-hatch directory + per-slice hook points in the generators.reverse-engineer intentcommand for migrating classic projects.Test plan
mvn -pl components/engine/engine-intent -am -P quick-build -DskipTests install- builds cleanmvn -pl components/engine/engine-intent formatter:validate- formatting cleanmvn -pl components/engine/engine-intent -P release -Dgpg.skip=true -DskipTests -Dlicense.skip=true -Dformatter.skip=true install- javadoc gate cleanCo-Authored-By: Claude Opus 4.7 noreply@anthropic.com
🤖 Generated with Claude Code