Skip to content

Add engine-intent: source-of-truth .intent artefact above EDM/BPMN/form#6017

Open
delchev wants to merge 22 commits into
masterfrom
engine-intent-scaffold
Open

Add engine-intent: source-of-truth .intent artefact above EDM/BPMN/form#6017
delchev wants to merge 22 commits into
masterfrom
engine-intent-scaffold

Conversation

@delchev

@delchev delchev commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Scaffolds a new authoring layer above the existing model artefacts: 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. 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 IntentTargetGenerator implementations, no IDE perspective. The design context is captured verbatim in components/engine/engine-intent/CLAUDE.md.

What's in

  • Intent JPA artefact (DIRIGIBLE_INTENTS, artefact type intent, file extension .intent) + repository + service.
  • IntentSynchronizer extends BaseSynchronizer; regeneration pass runs in finishing(). SynchronizersOrder.INTENT = 5, ahead of every other artefact, so the regenerated gen/ files participate on the next reconciliation cycle.
  • IntentTargetGenerator SPI + IntentRegenerationService + per-call IntentGenerationContext. Spring beans discovered via component scan, ordered with @Order. Each generator owns one slice and must be idempotent + scoped to <projectRoot>/gen/.
  • IntentModel POJOs for the v1 shape: entities (+ fields, relations), processes (+ steps), forms, reports, permissions.
  • IntentParser uses SnakeYAML's SafeConstructor (blocks !!type / !!new tags - 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.xml dependencyManagement, and components/group/group-engines/pom.xml.

Design notes (captured in detail in the module CLAUDE.md)

  • Format is YAML, not JSON. Optimised for human authoring (comments, multi-line strings, no quote noise, friendlier LLM diffs).
  • Expressiveness ceiling, LLM determinism, structured-not-free-text are the three things any non-trivial change to this engine must reckon with. The CLAUDE.md spells out why and what the chosen mitigations are (planned /custom/ escape-hatch, patch-shaped LLM edits, structured intent rather than free text).
  • Open design point: the orchestrator's repository walk runs once per cycle, so files written under gen/ in finishing() 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)

  • Concrete IntentTargetGenerator implementations per slice (entities, processes, forms, reports, permissions, controllers).
  • IDE perspective: Mermaid renderer (read-only) + Claude chat + patch preview + accept/reject.
  • /custom/ escape-hatch directory + per-slice hook points in the generators.
  • reverse-engineer intent command for migrating classic projects.
  • Same-cycle vs next-cycle visibility (open question above).
  • Schema validation on parse.

Test plan

  • mvn -pl components/engine/engine-intent -am -P quick-build -DskipTests install - builds clean
  • mvn -pl components/engine/engine-intent formatter:validate - formatting clean
  • mvn -pl components/engine/engine-intent -P release -Dgpg.skip=true -DskipTests -Dlicense.skip=true -Dformatter.skip=true install - javadoc gate clean
  • CI build + unit + integration tests
  • Smoke test once concrete generators land in a follow-up

Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com

🤖 Generated with Claude Code

delchev and others added 3 commits June 12, 2026 08:48
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>
delchev and others added 8 commits June 12, 2026 10:47
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>
@delchev delchev force-pushed the engine-intent-scaffold branch from 88247b1 to ef8aeee Compare June 12, 2026 11:46
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));
String fileName = report.getName() + ".report";
if (!seenFiles.add(fileName)) {
LOGGER.warn("Duplicate report [{}] in intent [{}] - keeping the first occurrence", report.getName(),
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()
delchev and others added 9 commits June 13, 2026 10:31
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>
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.

2 participants