Skip to content
Open
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
4 changes: 3 additions & 1 deletion .claude/docs/intent-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ A single `app.intent` YAML file at a project root is the source of truth one alt

**The reject-twin, and it reads the PARENT (`checks: forbidWhen`, [#7275](https://github.com/eclipse-dirigible/dirigible/issues/7275)):** `checks:` could *require*, *compare* and *count* but not "refuse this write while `<condition>` holds", and no check condition read a value one hop away - so the common rule was inexpressible: *forbid adding a child while its parent is in a given status* (a payment allocation cannot be added to an already PAID invoice). `immutableWhen` blocks EDITING and reads the record's OWN status (an allocation has none), `locksWithMaster` is all-or-nothing (the allocation stays addable while the invoice is ISSUED/SENT), and the over-allocation rollup guard is a money-safety side effect, not a domain-messaged "the invoice is paid" rule. `- { kind: forbidWhen, when: "SalesInvoice.Status == PAID", message: ... }` - the condition alone, no `field`/value. **Its one reach beyond `requiredWhen`** is that a `when` term may name a one-hop `Relation.field`, so a child tests its parent: walked by the same resolver, the hops loaded by FK first, and the status literal there resolving against the RELATION TARGET's nomenclature (a cross-model status name refused for its numeric id, as everywhere). The `status:` gate routes enforcement exactly as `requiredWhen`'s does - no gate = every user write (a 400 in all three controllers), a gate = the repository's `ValidationException` on the #7014/#7063 synchronous path. **The UI half hides the affordance, not only rejects it (like `fromStatus:` #7068):** when every term reads the composition master the master-detail panel already holds, a descriptor reaches the child's detail registration and the shared `detailPanel` hides Add / row edit / delete while the condition holds against the master record it was handed - no extra fetch, the server refusal still holds on every path. Refused at parse, each because it would otherwise be silent: a `field` on a forbidWhen, a missing `message`, a `when` term that does not resolve, a cross-model status by name, and a gate with no `function: EntityStatus` relation. Details in the engine-intent guide's forbidWhen bullet.

**Two values of one row, related (`checks: compare`, [#7095](https://github.com/eclipse-dirigible/dirigible/issues/7095)):** `checks:` knew `exactlyOne`, `itemsSumEqual` and `itemsMin` - nothing compared two fields of the SAME record, so "a due date is never before the invoice date" was not expressible and a document was saved (200), issued and overdue the moment it existed; the module's workaround was a `calculatedActionOnCreate`/`OnUpdate` class per document type that silently CORRECTED the date instead of refusing it, which is a different thing and never tells the clerk. `- { kind: compare, field: due, op: ge, than: date, message: ... }` is row-level like `exactlyOne`: enforced in every generated controller's `validate()` (the entity, personal and partner surfaces) as a 400 carrying the authored message, and therefore taking no `status` gate - a rule about two values of one row holds from the first save, not from a transition. `op:` is `ge`/`gt`/`le`/`lt`/`eq`/`ne`, spelled out because an omitted operator has no defensible default. Both operands are the entity's own **fields** - a comparison of two foreign keys means nothing - and must sit in ONE comparison family, which is what the generated code needs: two temporals compare through their own `compareTo` (a `LocalDate` does not compare to an `Instant`), two numbers by value through `BigDecimal` so a `decimal` against a `long` stays exact. Only dates, timestamps and numbers compare; a string / `month` / `week` is refused rather than silently ordered lexicographically, as is a field-with-itself. An **absent operand is not a violation** - a comparison is about two values that exist, and requiredness is its own declaration.
**Two values of one row, related (`checks: compare`, [#7095](https://github.com/eclipse-dirigible/dirigible/issues/7095)):** `checks:` knew `exactlyOne`, `itemsSumEqual` and `itemsMin` - nothing compared two fields of the SAME record, so "a due date is never before the invoice date" was not expressible and a document was saved (200), issued and overdue the moment it existed; the module's workaround was a `calculatedActionOnCreate`/`OnUpdate` class per document type that silently CORRECTED the date instead of refusing it, which is a different thing and never tells the clerk. `- { kind: compare, field: due, op: ge, than: date, message: ... }` is row-level by default, like `exactlyOne`: enforced in every generated controller's `validate()` (the entity, personal and partner surfaces) as a 400 carrying the authored message - a rule about two values of one row holds from the first save, not from a transition (the optional gate that routes it to the transition instead arrived with [#7338](https://github.com/eclipse-dirigible/dirigible/issues/7338), below). `op:` is `ge`/`gt`/`le`/`lt`/`eq`/`ne`, spelled out because an omitted operator has no defensible default. Both operands are the entity's own **fields** - a comparison of two foreign keys means nothing - and must sit in ONE comparison family, which is what the generated code needs: two temporals compare through their own `compareTo` (a `LocalDate` does not compare to an `Instant`), two numbers by value through `BigDecimal` so a `decimal` against a `long` stays exact. Only dates, timestamps and numbers compare; a string / `month` / `week` is refused rather than silently ordered lexicographically, as is a field-with-itself. An **absent operand is not a violation** - a comparison is about two values that exist, and requiredness is its own declaration.

**...and a field against a LITERAL (`checks: compare` with `value:`, [#7338](https://github.com/eclipse-dirigible/dirigible/issues/7338)):** all five check kinds related two things the model already NAMED - two fields of a row, two item sums, an item count - so the commonest validation in a business model had no declaration at all: `VacationDay.Days > 0` (a negative row silently inflates the parent entitlement, because the roll-up sums the column verbatim), a quantity `>= 0`, a percentage `<= 100`. The three workarounds in the fleet were each worse than the gap: a hand-edit of the generated controller's `validate()` (dropped by the next regeneration, silently), a `calculatedActionOnCreate` that throws (a calculation, not a refusal, firing only on the field that declares it and reaching the caller as whatever the action's exception carries), or not enforcing it at all. `- { kind: compare, field: days, op: gt, value: 0, message: ... }` reuses `compare` and its whole implementation: `value:` and `than:` are mutually exclusive and exactly one is required, since a comparison has one right-hand side. The literal is TYPED by the field it is compared with, by `CheckSupport.compareLiteral` - the one rule the parser refuses on and the generator renders with, so nothing is refused that would have generated and nothing generates that was not refused. A numeric field takes a number (compared by value through `BigDecimal`, exact across the widths); a temporal one takes a moment (`CURRENT_DATE` / `CURRENT_TIMESTAMP` / `NOW` with at most one signed ISO-8601 offset - the vocabulary `items: where:` already carries, resolved against the clock of the WRITE) or a quoted ISO-8601 date/instant, rendered in the shape the generated column actually carries (`LocalDate` for a `date`, `Instant` for a `timestamp`; a comparison across the two does not compile). An absent operand is not a violation, exactly as with `than:`. **The second half is the gate.** `compare` used to refuse a `status:`; it now takes the optional one `requiredWhen` has, and that is the routing: without a gate the rule holds on every user write (the three generated controllers, 400 with the authored message), with one it is the repository's and holds when the record is persisted CARRYING that status. "days > 0 before SUBMITTED" is the rule base-vacations actually needed and mis-authored as an `itemsMin`, which counted a child the approval delegate had not created yet and refused every submission in the field for three weeks.

**A form may show a field of its COUNTERPARTY ([#7093](https://github.com/eclipse-dirigible/dirigible/issues/7093)):** a task form's `fields` take one-hop `relation.field` paths, but the validator resolved the hop against LOCAL entities only - so the one relation a billing document's form most needs to read a field of, its counterparty, was the one it refused (`form [SendSalesInvoice] field [Customer.email] references unknown field [email] on [Customer]`), while the same path already resolved cross-model as a `notify` recipient and a `languageFrom`. A cross-model to-one is now resolved where every other cross-model reference is: at GENERATION, against the owner model's `.model`, which supplies the perspective the generated resolver's imports name and the key type behind its `Number` accessor - the delegate loads the OWNER's `gen.<model>.data.<perspective>` Entity/Repository, the registry-wide-compile mechanism a notify relation load already uses, and the control renders read-only like a local hop. A field the owner model does not declare is a **422** rather than a skipped resolver: skipping it would leave the BPMN with a service task pointing at a handler nothing generated and the control bound to a variable nothing ever sets. The same path in a `decision` condition comes with it, being one resolver.

Expand Down
Loading
Loading