Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ A single `app.intent` YAML file at a project root is the source of truth one alt

**Lifecycle-aware aggregates (`stage:` + `scope:` + symbolic statuses, [#6645](https://github.com/eclipse-dirigible/dirigible/issues/6645)):** a status seed row classifies what it MEANS to the lifecycle — `stage: draft|live|cancelled|void`, metadata never emitted as a column — and a `reports:` entry declares `scope: all` or a stage name, emitted as `<alias>."<STATUS FK>" IN (<stage ids>)` ANDed onto its filter. With the nomenclature classified, an **aggregating** report **defaults to `live`** unless its dimensions/`filter` already reference the status, so a draft or voided document stops silently inflating every total (an existing model is byte-identical until it adopts `stage:`). Every site that names a status — `transitions[].from`/`setStatus`, a relation's `init:`, `setRelationField` `value:`, `abortOn.status`, a check's `status`/`setStatus`, `immutableWhen`, a posting's `event.when`, a report's `filter` — additionally accepts the **seeded name**, resolved to the id on the raw YAML tree before the typed mapping: an id is positional, so inserting a status mid-nomenclature used to silently retarget every guard authored against the old numbering (how a red-storno posting guarded on `Status == 8` stopped matching a Void that writes 9, leaving the ledger with a receivable for a document that no longer existed). A cross-model nomenclature can be neither stage-scoped nor named (its seeds live in the owner model) and says so; and when a lifecycle-carrying aggregate has neither a scope nor a status predicate nor a classified nomenclature, Generate emits a **warning** — now shown in the Intent Editor's own strip, not only the Builder shell's. Details in the engine-intent guide's lifecycle bullet.

**The glue event axis: process-step events + non-HTTP inbound ([#6537](https://github.com/eclipse-dirigible/dirigible/issues/6537)):** a `notifications`/`integrations` `event:` binds either an entity lifecycle event or a **process step** — `onStepReached`/`onStepCompleted: { process, step }`. The step half adds no second delivery mechanism: `BpmnIntentGenerator` inserts a generated `JavaDelegate` at the step boundary (before the step for *reached*; after the writer/setter for *completed*, carrying the step's `next`) which loads the process's **trigger entity** by the id in the clear-D context and publishes its JSON on the entity's own topic plus a step suffix (`-step-<process>-<step>-reached|completed`), deferred to after commit — so the consumers are generated unchanged and every recipient path, `{placeholder}`, guard and forwarded body reads exactly as for a lifecycle event (`StepEventSupport` owns binding → entity → topic → emitter, deduplicated per moment). Rejected at parse: an unknown process/step, a step kind with no observable moment (only `userTask`/`serviceTask` have one), a process without a trigger. Alongside it, `inbound[]` declares **exactly one arrival** — `path:` (the `@Controller` webhook), `source: { queue | topic }` (a self-describing `MessageHandler`), or `source: { folder, cron }` (a `JobHandler` polling the drop folder: one record or an array per file, a file touched in the last few seconds is left for the next tick, every read file moves to `processed/`/`failed/`). All three save through the same generated repository — the arrival is a transport, not a second data path; a folder is polled, never watched, hence the mandatory cron there and its rejection elsewhere.

**Event-driven create-from (`generates` + `event:`, [#6711](https://github.com/eclipse-dirigible/dirigible/issues/6711)):** a `generates` entry may declare `event: { onTransition: <from>, when: "<Status> == <status>" }` (guard mandatory) or `{ onCreate: <from> }` and mint the follow-up **document — header AND items** by itself when the source reaches a state, instead of waiting for the button (`posts` is event-driven but emits flat rows and cannot reference the new header). The `map` entry copying the source's key IS the back-reference and therefore the **at-most-once** guard, derived rather than declared twice; the button is dropped unless `button: true`, and both triggers share ONE generated create-from (a new `GenerateOnEvent.java.template` listener calls `<X>Generate.create(id)` and carries no mapping of its own). Details in the engine-intent guide's `event:` bullet.

**The general platform line this enshrines:** authoring artifacts (`.edm`, `.model`, `.form`, `.report`, `.intent`) get **workspace editors + an explicit Generate**; only runtime artifacts (`.roles`, `.bpmn`, `.csvim`, `.table`, jobs, listeners, …) get **synchronizers**. Applying the synchronizer hammer to an authoring artifact generates into the registry where no modeler, Projects view, or template can use it — that mistake was made once and reverted; the inventory of synchronizers (grep `extends BaseSynchronizer`) deliberately contains no authoring formats.
Expand Down
10 changes: 9 additions & 1 deletion components/engine/engine-intent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -554,8 +554,16 @@ Every action below has a real SDK surface to generate against, so none of this n
### Sequencing
Build **#1 (reactions) + #2 (notify)** first: one new concept ("reaction"), reuses trigger + resolver, unlocks the most apps per line of new code. Then **#3 (schedules)** (`sdk.job.Scheduled` already exists). **#6 (state machine)** is the highest later-leverage item but needs the most design. Each ships behind a `.settings` override and a parser-validated binding grammar.

### The event axis (#6537) — a step event is delivered as an entity event

`notifications` and `integrations` bind to **either** an entity lifecycle event (`onCreate`/`onUpdate`/`onDelete`: `<Entity>`) **or** a process step event (`onStepReached`/`onStepCompleted`: `{ process, step }`). The step half is deliberately NOT a second delivery mechanism: `BpmnIntentGenerator` inserts a generated `JavaDelegate` at the step boundary (before the step for *reached*, after the writer/setter for *completed*, carrying the step's `next` so routing can't be bypassed) which loads the process's **trigger entity** by the id in the clear-D context and publishes its JSON to the entity's own topic plus a step suffix (`-step-<process>-<step>-reached|completed`), deferred to after-commit. So every consumer — the notification's relation loads, guards, `{placeholder}` interpolation and print attachments; the integration's forwarded body — is generated **unchanged**, reading a payload of the same shape as a lifecycle event. `StepEventSupport` owns the whole translation (binding → entity → topic suffix → emitter), deduplicated per (process, step, moment) so ten consumers of one moment still publish once.

Consequences worth knowing: a step event is only as meaningful as the process's `trigger` (no trigger → no record → the parser rejects the binding), and only a `userTask`/`serviceTask` is eventable (a decision/wait/end occupies no moment). `EventBinding` stays lifecycle-only — `StepEventSupport.eventEntity`/`topicSuffix` are the two polymorphic entry points the glue builders call.

The inbound half of the same issue: `inbound[]` declares **exactly one arrival** — `path:` (the existing `@Controller` webhook), or `source: { queue | topic }` (a self-describing `MessageHandler`, `InboundMessage.java.template`), or `source: { folder, cron }` (a `JobHandler` polling the drop folder, `InboundFile.java.template`: one record or an array per file, a file untouched for less than 5s is left for the next tick, every read file is moved into `processed/` or `failed/`). All three deserialize into the same entity and save through the same repository, so validations, i18n and the create event fire identically. There is no file-system watch — a folder is polled, which is why the cron is mandatory there and rejected elsewhere.

### Status of the catalog (what one `app.intent` can declare today)
Implemented and generating annotated client-Java off the shared `EventBinding` / `NotificationSupport` / `ScheduleSupport` / `Criteria` core: **notifications** (#2, email; direct field / one-hop `relation.field` / literal; `when` guard), **schedules** (#3, cron → typed-`Criteria` query → per-row notify), **outbound integrations** (#4, event → `HttpClient`), **inbound webhooks** (#5, `@Controller` ingest → entity), **lifecycle triggers** (process `trigger` on `onCreate`/`onUpdate`/`onDelete` + `when` guard, with a configurable `businessKey` field and an optional `businessKeyStrategy: timestamp`), **rollups** (#9, recompute a parent counter via `Criteria`), and **dynamic task assignment** (#10, `assignee: { path, fallback }` - a to-one walk off the trigger record resolved at task entry). Still open / blocked: **documents** (#7 - the PDF engine is XSLT/XSL-FO, needs an HTML→PDF path first), **state machine** (#6 - needs write-path transition enforcement, deeper than a listener), **audit/history** (#8 - needs a generated shadow entity).
Implemented and generating annotated client-Java off the shared `EventBinding` / `StepEventSupport` / `NotificationSupport` / `ScheduleSupport` / `Criteria` core: **notifications** (#2, email; direct field / one-hop `relation.field` / literal; `when` guard), **schedules** (#3, cron → typed-`Criteria` query → per-row notify), **outbound integrations** (#4, event → `HttpClient`), **process step events** (#6537, `onStepReached`/`onStepCompleted` on both of the above), **inbound ingest** (#5, `@Controller` webhook / queue-topic `MessageHandler` / polled-folder `JobHandler` → entity), **lifecycle triggers** (process `trigger` on `onCreate`/`onUpdate`/`onDelete` + `when` guard, with a configurable `businessKey` field and an optional `businessKeyStrategy: timestamp`), **rollups** (#9, recompute a parent counter via `Criteria`), and **dynamic task assignment** (#10, `assignee: { path, fallback }` - a to-one walk off the trigger record resolved at task entry). Still open / blocked: **documents** (#7 - the PDF engine is XSLT/XSL-FO, needs an HTML→PDF path first), **state machine** (#6 - needs write-path transition enforcement, deeper than a listener), **audit/history** (#8 - needs a generated shadow entity).

**The canonical, verified showcase is `IntentEngineIT`'s `INTENT_YAML` fixture** - a single Orders `app.intent` that declares entities (incl. a `setting` + a composition), a process (trigger + decision/resolver + user/service tasks), forms, reports, roles, seeds, **and** every glue block above; the `glue_template_generates_the_trigger_and_resolver_handlers` test generates from it and asserts the whole catalog (trigger, resolver, notification, schedule job, integration, webhook, rollup×2) is produced from that one file. Mirror it into `dirigiblelabs/sample-intent-model` when publishing a runnable sample.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ public static String kind(Map<String, Object> event) {

/**
* @param event the binding map (may be {@code null})
* @return the entity named by the bound event, or {@code null}
* @return the entity named by the bound event, or {@code null} - including when the binding is not
* an entity lifecycle one at all (a {@code onStepReached}/{@code onStepCompleted} binding
* names a process step, not an entity; see {@link StepEventSupport})
*/
public static String entity(Map<String, Object> event) {
String kind = kind(event);
Object target = kind == null || event == null ? null : event.get(kind);
return target == null ? null : target.toString();
return target instanceof Map<?, ?> || target == null ? null : target.toString();
}

/**
Expand Down
Loading
Loading