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: 1 addition & 1 deletion components/engine/engine-intent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ Implemented and generating annotated client-Java off the shared `EventBinding` /
- **Parser hardening.** A wrong-typed scalar (e.g. an unquoted brace recipient `to: {member.email}`, which YAML parses as an object) now surfaces as a clean `IntentValidationException` issue with a helpful message instead of a raw 500 Gson error that wedged the editor. `IntentParserTest` covers it.
- **Externalized AI system prompt.** Moved from an inline string to `intent-assistant-guide.md` (classpath resource, fail-fast load), corrected to the full current schema incl. the glue catalog + `businessKey`/`businessKeyStrategy`, and restored the propose-the-whole-file tool contract the draft had dropped.
- **Settlements re-allocate a corrected payment (#6818).** The payment spread handler used to bind the payment's bare create topic only, so a payment booked for the wrong amount and corrected afterwards - or created incomplete and completed later - was never re-allocated and the invoice kept the original settled figure. It is now emitted once per bound payment event (create + `-updated`) from its own glue collection, **`settlementListeners`** - the `settlements` collection still drives the one-per-settlement `<Name>OnInvoice` delegate, and a second collection is what lets the two templates fan out differently while sharing one descriptor (`rollupEntry` copies it per class name + topic suffix, exactly as roll-ups and expansions do). Note the two traps a new glue collection carries: it needs a `case` in `GlueGenerator` (Java - the only generator since #6707; the old `generateUtils.js` twin is gone) or it renders whole-model with raw `${...}` placeholders, and `GlueGenerator.copy` is a per-key allow-list, so a new descriptor key that is not listed there never reaches the template. The handler itself was already a recompute of the payment's *unallocated* balance (re-delivery is a no-op by construction); it now also **releases** the excess - newest allocation first, through the junction repository - when the payment is corrected below what it already covers, so the recompute converges in both directions. Two boundaries of that release: the CREATE handler never releases - a create event is the first word about a payment, so a negative pot there can only be a DELAYED create racing a correction the updated handler already allocated, and releasing on the stale payload would undo it (#6865). And a corrected MATCH column (the payment re-filed under another Customer) is invisible to the amount-based recompute (`pot - allocated == 0`), so the payment's match columns are grouping keys and a third listener on the payment's `-rekeyed` topic releases everything and re-allocates from the STORE - both re-key notices run the same store-driven recompute, so delivery order cannot matter; emitted only for a LOCAL payment, since a cross-model payment's DAO belongs to the owner model and a projection has no repository to re-read (#6864).
- **What a Duplicate does NOT copy (`duplicable` object form, #7358).** `duplicable: true` cloned every ordinary user field of the header, so a copied invoice kept the source's `date`, `due` and `taxEventDate` - "same invoice as last month" opened dated last month, and the module could not fix it on its own side: a `calculatedActionOnCreate` fills an EMPTY value and respects a present one by contract, which is exactly what makes the copied value stick. The key now also takes `{ defaults: {...}, reset: [...] }` - `reset` drops a field from the clone so the create path fills it as it would on a hand-made document, `defaults` writes a constant, with the same `now` token and the same field-shape rendering `generates.defaults` has (`date` -> `YYYY-MM-DD`, `month` -> `YYYY-MM`, `week` -> `YYYY-Www`). The shorthand is normalized to the empty object on the RAW tree (`IntentParser.normalizeDuplicable`, the `expandUniqueShorthand` precedent) so one typed class carries both forms and the unknown-key walk sees the two keys; `EntityIntent.duplicable` is therefore a `DuplicateIntent`, not a `Boolean`. Both halves reach the document template as **structured** `.edm` attributes (`duplicateReset`, `duplicateDefaults` in `STRUCTURED_ATTRIBUTES` and in `transform-edm.js`'s `ENTITY_STRUCTURED`) - entity metadata a flat attribute cannot carry is lost on the next modeler save (#6826), which here would silently put the copied dates back. Refused at parse, each because accepting it would be silent: a name that is neither a field nor a to-one of the entity, one of the built-in drops (identity, `number:`, `function: EntityStatus`, `readOnly`, `aggregate`), the same name in both lists, `now` on a property that is not a date/month/week, and a `reset` of a **required** field with no `defaultValue` and no create-time rule - that one would make every duplicate fail on the server's own "field is required". The `now` value renders from the LOCAL calendar fields in `todayAs`, never `toISOString()`: east of Greenwich that is yesterday after the evening cut-over. Out of scope and still true: the copy is client-side and not atomic - a failed line POST leaves a half-copied draft.
- **What a Duplicate does NOT copy (`duplicable` object form, #7358).** `duplicable: true` cloned every ordinary user field of the header, so a copied invoice kept the source's `date`, `due` and `taxEventDate` - "same invoice as last month" opened dated last month, and the module could not fix it on its own side: a `calculatedActionOnCreate` fills an EMPTY value and respects a present one by contract, which is exactly what makes the copied value stick. The key now also takes `{ defaults: {...}, reset: [...] }` - `reset` drops a field from the clone so the create path fills it as it would on a hand-made document, `defaults` writes a constant, with the same `now` token and the same field-shape rendering `generates.defaults` has (`date` -> `YYYY-MM-DD`, `timestamp` -> the ISO instant, `month` -> `YYYY-MM`, `week` -> `YYYY-Www`). The shorthand is normalized to the empty object on the RAW tree (`IntentParser.normalizeDuplicable`, the `expandUniqueShorthand` precedent) so one typed class carries both forms and the unknown-key walk sees the two keys; `EntityIntent.duplicable` is therefore a `DuplicateIntent`, not a `Boolean`. Both halves reach the document template as **structured** `.edm` attributes (`duplicateReset`, `duplicateDefaults` in `STRUCTURED_ATTRIBUTES` and in `transform-edm.js`'s `ENTITY_STRUCTURED`) - entity metadata a flat attribute cannot carry is lost on the next modeler save (#6826), which here would silently put the copied dates back. Refused at parse, each because accepting it would be silent: a name that is neither a field nor a to-one of the entity, one of the built-in drops (identity, `number:`, `function: EntityStatus`, `readOnly`, `aggregate`), the same name in both lists, `now` on a property that does not hold a moment (not a date/timestamp/month/week - #7396 added the timestamp shape, which the check listed out of the three it had a renderer for although it is the one value an author would write on such a field), and a `reset` of a **required** field with no `defaultValue` and no create-time rule - that one would make every duplicate fail on the server's own "field is required". The `now` value renders from the LOCAL calendar fields in `todayAs`, never `toISOString()`: east of Greenwich that is yesterday after the evening cut-over - except the `timestamp` shape, which IS an instant and not a wall clock, and carries the ISO instant the form's own `toPayload()` sends for a datetime input, since a `java.time.Instant` property does not bind the local calendar shapes at all. Out of scope and still true: the copy is client-side and not atomic - a failed line POST leaves a half-copied draft.
- **CI runs on Corretto 24** (compile target stays 21); the integration-test fork gets `-Xmx6g`. (Root-level change; recorded here because it landed alongside the intent work.)

**Cross-artefact field naming:** the `.form` control `model` (and control `id`) bind to the entity property, so they use `IntentNaming.pascalCase` to match the EDM property names (`loanedOn` -> `LoanedOn`). The `.report` references physical UPPER_SNAKE columns and humanized display aliases (no camelCase property identifiers), so it needs no PascalCasing.
2 changes: 1 addition & 1 deletion components/engine/engine-intent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Entity-level extras: `order: [Id, Product, Quantity, ...]` sequences form contro
`duplicable: true` adds a Duplicate button on a document (clones header + items through the normal
create path), and its object form says what the copy must NOT carry over - `duplicable: { defaults: {
date: now }, reset: [due, taxEventDate] }`, where `reset` hands a field back to the entity's own
create-time rule and `defaults` writes a constant (`now` is today in the field's own shape); `imports: |` injects Java import lines into the generated repository (pairs with
create-time rule and `defaults` writes a constant (`now` is the current moment in the field's own shape); `imports: |` injects Java import lines into the generated repository (pairs with
calculated actions); `aggregate: true` on a document master's numeric field keeps it equal to the
sum of the items' same-named field (the totals footer).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3613,12 +3613,18 @@ private static List<String> duplicateResets(EntityIntent entity) {
return resets;
}

/** The authored field types a {@code now} default renders in their own shape. */
private static final Set<String> NOW_SHAPES = Set.of("date", "timestamp", "month", "week");

/**
* The constants a Duplicate writes into the cloned header, as {@code {name, shape, js}} entries in
* authored order. {@code shape} is {@code date} / {@code month} / {@code week} for the {@code now}
* token - today in the field's own shape, rendered by the document page's {@code todayAs} helper
* against the LOCAL clock - and {@code literal} otherwise, where {@code js} carries the value
* already coerced to the property's type as a JavaScript literal.
* authored order. {@code shape} is {@code date} / {@code timestamp} / {@code month} / {@code week}
* for the {@code now} token - the current moment in the field's own shape, rendered by the document
* page's {@code todayAs} helper against the LOCAL clock - and {@code literal} otherwise, where
* {@code js} carries the value already coerced to the property's type as a JavaScript literal. The
* shape is the AUTHORED type, not a narrowing of it: a {@code timestamp} property binds a
* {@code java.time.Instant} on the server, which the {@code YYYY-MM-DD} a {@code date} shape
* produces does not fill (#7396).
*
* @param entity the duplicable document master
* @return the entries, never null
Expand All @@ -3637,7 +3643,7 @@ private static List<Map<String, Object>> duplicateDefaults(EntityIntent entity)
entry.put("name", IntentNaming.pascalCase(name.trim()));
String type = duplicateDefaultType(entity, name.trim());
if ("now".equals(value.trim())) {
entry.put("shape", "month".equals(type) || "week".equals(type) ? type : "date");
entry.put("shape", NOW_SHAPES.contains(type) ? type : "date");
entry.put("js", "");
} else {
entry.put("shape", "literal");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,17 @@
* The two keys compose and never overlap: {@code reset} is for a field that HAS a create-time rule
* (a {@code calculatedActionOnCreate}, a {@code defaultValue}) and must be handed back to it - a
* copied value would be respected by that rule and stick; {@code defaults} is for a field that has
* none, where the copy needs a value stated here. {@code now} renders today in the field's own
* shape, the same token and the same rendering {@code generates.defaults} uses.
* none, where the copy needs a value stated here. {@code now} renders the current moment in the
* field's own shape, the same token and the same rendering {@code generates.defaults} uses.
*/
public class DuplicateIntent {

/**
* Constants written into the cloned header after the resets, by the entity's own field / to-one
* relation name. {@code now} is today in the field's shape ({@code date} -> {@code YYYY-MM-DD}, a
* {@code month} field -> {@code YYYY-MM}, a {@code week} field -> {@code YYYY-Www}); any other
* value is a literal coerced to the property's type.
* relation name. {@code now} is the current moment in the field's shape ({@code date} ->
* {@code YYYY-MM-DD}, a {@code timestamp} field -> the ISO instant, a {@code month} field ->
* {@code YYYY-MM}, a {@code week} field -> {@code YYYY-Www}); any other value is a literal coerced
* to the property's type.
*/
private Map<String, String> defaults = new LinkedHashMap<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -718,9 +718,17 @@ private static void validateDuplicableReset(String subject, String name, Relatio
}

/**
* A {@code defaults} value: {@code now} is today in the field's own shape, so it is only meaningful
* on a field that HOLDS a date - the same rule and the same wording {@code generates.defaults}
* uses. Anything else is a literal, coerced to the property's type at generation.
* The field types a {@code now} default is meaningful on: the ones that HOLD the current moment,
* each rendered in its own shape. A {@code timestamp} is one of them - it differs from a
* {@code date} only in precision, and the copy of a document is made now in both cases (#7396).
*/
private static final Set<String> NOW_FIELD_TYPES = Set.of("date", "timestamp", "month", "week");

/**
* A {@code defaults} value: {@code now} is the current moment in the field's own shape, so it is
* only meaningful on a field that HOLDS one - the same rule and the same wording
* {@code generates.defaults} uses. Anything else is a literal, coerced to the property's type at
* generation.
*/
private static void validateDuplicableDefault(String subject, String name, String type, String value, List<String> issues) {
if (value == null || value.isBlank()) {
Expand All @@ -731,9 +739,9 @@ private static void validateDuplicableDefault(String subject, String name, Strin
return;
}
String kind = type == null ? "" : type.toLowerCase(Locale.ROOT);
if (!"date".equals(kind) && !"month".equals(kind) && !"week".equals(kind)) {
issues.add(subject + " assigns [" + name + "] the value now, but that property is not a date - now is today in the field's own"
+ " shape, so it is only a value for a date / month / week field");
if (!NOW_FIELD_TYPES.contains(kind)) {
issues.add(subject + " assigns [" + name + "] the value now, but that property does not hold a moment - now is the current"
+ " moment in the field's own shape, so it is only a value for a date / timestamp / month / week field");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -806,18 +806,18 @@ value and respect a present one. Say so with the object form:

`reset:` is for a field that HAS a create-time rule (a `calculatedActionOnCreate`, a `defaultValue`)
and must be handed back to it; `defaults:` is for a field that has none, where the copy needs a value
stated here. `now` is today in the field's own shape (a `date` field -> `YYYY-MM-DD`, a `month` field
-> `YYYY-MM`, a `week` field -> `YYYY-Www`), the same token `generates.defaults` takes; any other
value is a literal coerced to the property's type. Both keys name the entity's own fields and to-one
relations - no `relation.field` paths.
stated here. `now` is the current moment in the field's own shape (a `date` field -> `YYYY-MM-DD`, a
`timestamp` field -> the ISO instant, a `month` field -> `YYYY-MM`, a `week` field -> `YYYY-Www`), the
same token `generates.defaults` takes; any other value is a literal coerced to the property's type.
Both keys name the entity's own fields and to-one relations - no `relation.field` paths.

Refused at parse: a name that is neither a field nor a to-one relation of the entity; one that is
already dropped anyway (the primary key, the `number:` field, the `function: EntityStatus` relation,
a `readOnly` or an `aggregate` field) - naming it would let you believe you control something the
Duplicate decided long before reading the block; the same name in both lists; `now` on a property
that is not a date / month / week; and a `reset` on a **required** field with neither a
`defaultValue` nor a create-time rule, which would make every duplicate fail on the server's own
"field is required".
that does not hold a moment (not a date / timestamp / month / week); and a `reset` on a **required**
field with neither a `defaultValue` nor a create-time rule, which would make every duplicate fail on
the server's own "field is required".

**Control order (`order:`):** by default the generated UI controls (form inputs, list columns, detail
rows) follow the declaration order - all fields first, then the to-one relations, so relations end up
Expand Down Expand Up @@ -3111,8 +3111,8 @@ EmployeeTimesheet for each active employee". Per matching row, a new target reco
saved through the target's generated repository, so its create-time logic (document numbering, status
init, calculated fields) fires. The **row is the source**, so `from` is implicit (the schedule's
`entity`); `map` copies a field or to-one relation of the row onto a target property, `defaults` sets
`now` (rendered in the target field's own shape - date / `YYYY-MM` month / `YYYY-Www` week) or a
literal. The target may live in another model via `uses:` (same as `generates`).
`now` (rendered in the target field's own shape - date / instant / `YYYY-MM` month / `YYYY-Www` week)
or a literal. The target may live in another model via `uses:` (same as `generates`).

```yaml
schedules:
Expand Down
Loading
Loading