Skip to content

intent: a duplicable document says which fields a copy resets or defaults (#7358) - #7360

Merged
delchev merged 1 commit into
masterfrom
issue-7358-duplicable-defaults-reset
Sep 13, 2026
Merged

intent: a duplicable document says which fields a copy resets or defaults (#7358)#7360
delchev merged 1 commit into
masterfrom
issue-7358-duplicable-defaults-reset

Conversation

@delchev

@delchev delchev commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

The defect

duplicable: true is a bare boolean. The generated Duplicate clones the source header verbatim - minus identity, audit, status, number, read-only and aggregate fields - and posts the rest through the normal create path, so every ordinary user field rides along, including the ones a business rule says must be fresh. On a sales invoice the copy therefore carries the source's date, due and taxEventDate: "same invoice as last month" opens as a draft dated last month, due last month, with last month's tax event. The user has to notice and fix three dates by hand, and an unnoticed one is a legally wrong document (the tax-event date is mandatory on BG invoices).

The module could not fix this on its own side. The two create-time actions on the invoice (due and taxEventDate, both calculatedActionOnCreate) fill an empty value and respect a present one by contract - the right contract for a hand-made invoice, and exactly what makes the copied value stick - and date is a plain required field with no default at all.

The change

duplicable keeps the shorthand and also accepts an object form:

- name: SalesInvoice
  duplicable:
    defaults: { date: now }        # constants written into the clone
    reset: [due, taxEventDate]     # dropped, so the entity's own create-time rule refills them

reset: hands a field back to the create path, which fills it exactly 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 already has (date -> YYYY-MM-DD, a month field -> YYYY-MM, a week field -> YYYY-Www). They compose and never overlap - reset for a field that HAS a create-time rule, defaults for one that does not. Everything not named is copied, as today. The built-in drops stay exactly as they were; they were never authorable.

Three things worth reviewing:

  • The shorthand is normalized on the RAW tree (IntentParser.normalizeDuplicable, the expandUniqueShorthand / moveGeneratesItemLines precedent): true becomes the empty mapping, false is removed. One typed class (DuplicateIntent) then carries both forms, the unknown-key walk sees defaults / reset as declared fields, and Gson never has to map a boolean onto an object. EntityIntent.duplicable is therefore a DuplicateIntent, not a Boolean; isDuplicable() is unchanged for every caller.
  • Both halves travel as STRUCTURED .edm attributes (duplicateReset, duplicateDefaults - registered in EdmIntentGenerator.STRUCTURED_ATTRIBUTES and in transform-edm.js's ENTITY_STRUCTURED). Entity metadata a flat attribute cannot carry is dropped on the .edm -> .model rebuild (The .edm must stay a lossless source for the derived .model: saving a diagram drops rollupGuard/checks/labelParts/uniqueConstraints, and the editor serializer's allow-list drops the intent-era attributes wholesale #6826), and here that loss would silently put the copied dates back on the first unrelated modeler save.
  • todayAs builds from the LOCAL calendar fields, never toISOString(): that is UTC, so east of Greenwich every copy made after the evening cut-over would be dated yesterday.

Refused at parse

Each because accepting it would be silent:

  • a name that is neither a field nor a to-one relation of the entity;
  • one of the built-in drops - the primary key, the number: field, the function: EntityStatus relation, a readOnly or an aggregate field - which the Duplicate decided long before reading this block, so naming it would let an author believe they control something they do not;
  • the same name in both reset and defaults (a contradiction);
  • now on a property that is not a date / month / week;
  • a reset on a required field with neither a defaultValue nor a create-time rule, which would make every duplicate fail with the server's own "field is required";
  • and a duplicable scalar that is neither true/false nor a mapping.

Verified

  • DuplicableIntentTest - 16 cases: the shorthand still parses and carries no rules, false is not duplicable, the object form parses, and each refusal above has a case asserting its message.
  • EdmIntentGeneratorTest - two new cases: the object form emits the resets (as generated property names) and the defaults ({name, shape, js}, now resolved per field shape, a string literal quoted) and the .edm carries both as JSON attributes; the shorthand emits neither key.
  • The engine-intent unit suite: 1280 tests green.
  • IntentEmissionCoverageIT (green, 149s) - a new Reorder document with the object form; the assertions read the generated document page and require delete header['Note'];, header['OrderedOn'] = this.todayAs('date');, header['Period'] = this.todayAs('month');, header['Comment'] = "Copy"; and a local-calendar todayAs.
  • EdmModelRoundTripIT (green, both tests) - a duplicable document added to the fixture; both keys are asserted present in the .edm, structurally equal after the .edm -> .model rebuild, and covered by the whole-document assertEveryKeySurvives diff.
  • mvn formatter:validate with the cache wiped, and the -P release javadoc build on engine-intent: no errors.

Out of scope (deliberately not done here)

  • Making the copy atomic / server-side. Today a failed line POST still leaves a half-copied draft; a <Entity>Duplicate glue controller in the pattern of <Entity>Generate would fix that - separate issue.
  • Copying attachments, comments or history.
  • A generic defaultValue: now on date fields (it would let reset: [date] replace defaults: { date: now }) - worth its own issue.
  • The Intent specification row on intentfile.org: a proposal PR follows.

Fixes #7358

🤖 Generated with Claude Code

…ults (#7358)

`duplicable: true` cloned the source header verbatim, so every ordinary
user field rode along - including the ones a business rule says must be
fresh. On a sales invoice the copy carried the source's `date`, `due` and
`taxEventDate`, so "same invoice as last month" opened as a draft dated
last month, due last month, with last month's tax event; an unnoticed one
is a legally wrong document.

The module could not fix it on its own side: the two create-time actions
fill an EMPTY value and respect a present one by contract - the right
contract for a hand-made invoice, and exactly what makes the copied value
stick - and `date` had no rule at all.

`duplicable` now also takes the object form

    duplicable:
      defaults: { date: now }        # constants written into the clone
      reset: [due, taxEventDate]     # dropped, so the create-time rule refills them

`reset` hands a field back to the create path, which fills it exactly 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, a `month` field -> YYYY-MM, a `week` field ->
YYYY-Www). The shorthand is normalized to the empty object on the raw
tree (the `expandUniqueShorthand` precedent), so one typed class carries
both forms, the unknown-key walk sees the two keys, and an entity that
says nothing generates byte-identically.

Both halves travel as STRUCTURED `.edm` attributes (`duplicateReset`,
`duplicateDefaults`), because entity metadata a flat attribute cannot
carry is lost on the next modeler save (#6826) - which here would
silently put the copied dates back. `todayAs` builds from the local
calendar fields, never `toISOString()`: east of Greenwich that is
yesterday after the evening cut-over.

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, the `number:` field, the `function: EntityStatus` relation, a
`readOnly` or an `aggregate` field), which the Duplicate decided long
before reading this block; 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, which would make
every duplicate fail on the server's own "field is required".

Verified: DuplicableIntentTest (16 cases), EdmIntentGeneratorTest's two
new emission cases, the engine-intent unit suite (1280), and both ITs -
IntentEmissionCoverageIT, which asserts the generated document page
carries the delete and the two `todayAs` assignments, and
EdmModelRoundTripIT, which proves both keys survive .intent -> .edm ->
.model.

Fixes #7358

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
delchev added a commit to dirigible-io/dirigible-io.github.io that referenced this pull request Sep 12, 2026
…(#7358) (#245)

`duplicable: true` clones the source header verbatim, so a copied invoice
keeps the source's date, due date and tax-event date - and a
`calculatedActionOnCreate` cannot repair it, because those fill an empty
value and respect a present one. Document the object form (`defaults:` +
`reset:`), the `now` token's per-field shape, what stays copied, and the
seven parse refusals.

Platform PR: eclipse-dirigible/dirigible#7360

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@delchev
delchev merged commit 6a7768b into master Sep 13, 2026
10 checks passed
@delchev
delchev deleted the issue-7358-duplicable-defaults-reset branch September 13, 2026 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant