Skip to content

intent: a checks kind for two relations of a junction row that must agree on a shared target (#7409) - #7418

Merged
delchev merged 2 commits into
masterfrom
issue-7409-checks-agree
Sep 17, 2026
Merged

delchev merged 2 commits into
masterfrom
issue-7409-checks-agree

Conversation

@delchev

@delchev delchev commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

The gap

A junction/allocation entity links two records that both point at the same third thing - the same Customer, the same Currency, the same Company, the same Employee. No checks: kind could say "these two relations must agree on X": compare relates two values of ONE row, and requiredWhen/forbidWhen relate a child to its OWN parent. Nothing compared two DIFFERENT relations' targets.

So a EUR CustomerPayment was allocated against a USD SalesInvoice (200 OK), and customer B's payment against customer A's invoice (200 OK). The probe found the same shape in 7 more places across 6 modules - PurchaseInvoicePayment, StockTransfer, EmployeeTimesheet, EmployeeProjectAssignment, Payslip, VacationDay - every one closed by a hand-written calculatedActionOnCreate/OnUpdate guard class whose entire content was a rule the intent should be able to state.

The change

checks:
  - { kind: agree, relations: [SalesInvoice, CustomerPayment], onProperty: Customer,
      message: "This payment belongs to a different customer than the invoice" }
  - { kind: agree, relations: [SalesInvoice, CustomerPayment], onProperty: Currency,
      message: "The payment's currency does not match the invoice's" }
  • relations: names exactly two distinct to-one relations of the entity; onProperty: the property BOTH their targets declare - one of their to-ones (compared by its foreign key: the same customer, the same currency) or a scalar field an equality is exact on.
  • Each side is ONE PATH - <relation>.<onProperty> through the same ResolvePathSupport walker every other path in the DSL uses, sharing ONE walker so each related record is loaded exactly once and a cross-model target resolves through its uses: owner like any other hop.
  • Enforced in the generated validate() of all three surfaces' controllers (EntityController, EntityMyController, EntityPartnerController) with 400 and the authored message - the same place exactlyOne and an ungated compare refuse - so it takes no status: gate.
  • whenNull: defaults to skip (a row not yet carrying both values has nothing to disagree about, and requiredness is the relation's own declaration); refuse rejects the write instead.
  • A check with no authored message still says what disagreed - only the declaration knows.

One deviation from the proposal: the key is onProperty, not on

YAML 1.1 resolves a bare on key to the boolean true, so an on: Customer declaration arrives as the key true, binds to nothing, and would generate a check with no property to agree on - the trap lifecycle: already documents. Since the issue proposes on:, authors and the assistant will write it, so that spelling is refused by name on the raw tree (rejectCheckOn) pointing at onProperty, rather than dropped silently.

Refused at parse

Each because the declaration could not otherwise mean anything: not exactly two relations; the same relation twice (it always agrees with itself); a relation or an onProperty a target does not declare (the walker's own message, so a cross-model target reads); a status: gate; an unknown whenNull; a terminal an equality is not exact on (a decimal, a double, a date - the same line a when condition draws); and two terminals of DIFFERENT declared types, where the boxed comparison is silently always false.

An entity declaring no agree check generates byte-identically.

Verified

  • IntentParserTest.agreeChecksParseAndValidate - the shape and every refusal above, incl. the on: spelling.
  • EdmIntentGeneratorTest.agreeChecksEmitBothSidesAndTheirLoads - both expressions, both labels, the shared walker loading each side once, and the derived message.
  • IntentEmissionCoverageIT - a junction entity added to the fixture carrying BOTH whenNull readings; the generated controller is compiled and asserted to load each side by FK and refuse a disagreement. Run green locally (Tests run: 1, Failures: 0, Errors: 0, 330 s).
  • Full unit suites of engine-intent (1317) and ide-template (169) green; mvn formatter:validate green with the cache wiped; -P release javadoc build green on both modules.

Not verified: no browser-level check - an agree refusal surfaces through the same 400 path every other row check already uses, and no UI half is emitted.

Follow-ups outside this PR, per the issue: the intent-assistant guide is updated here, the IntentFile spec proposal follows now that the shape is agreed, and R13 can turn from WAITING-UPSTREAM into a real FINDING.

Fixes #7409

🤖 Generated with Claude Code

delchev and others added 2 commits September 17, 2026 10:34
…gree on a shared target (#7409)

A junction/allocation entity links two records that both point at the same
third thing - the same Customer, the same Currency, the same Company - and no
`checks:` kind could say so: `compare` relates two values of ONE row, and
`requiredWhen`/`forbidWhen` relate a child to its OWN parent. So a EUR
CustomerPayment of customer B was allocated against a USD SalesInvoice of
customer A and both writes answered 200; a fleet probe found the same shape in
7 more places across 6 modules, every one closed by a hand-written
calculatedActionOnCreate/OnUpdate guard class whose entire content was a rule
the intent should be able to state.

    checks:
      - { kind: agree, relations: [SalesInvoice, CustomerPayment],
          onProperty: Customer, message: "..." }

`relations:` names exactly two distinct to-one relations of the entity and
`onProperty:` the property BOTH their targets declare - one of their to-ones
(compared by its foreign key) or a scalar an equality is exact on. Each side is
ONE PATH - `<relation>.<onProperty>` through the same ResolvePathSupport walker
every other path in the DSL uses, sharing one walker so each related record is
loaded exactly once and a cross-model target reads through its `uses:` owner
like any other hop - and the comparison is emitted into the three controllers'
validate() as a 400 with the authored message. `whenNull:` defaults to `skip`
(a row not yet carrying both values has nothing to disagree about, and
requiredness is the relation's own declaration); `refuse` rejects instead.

The key is `onProperty`, not the proposed `on`: YAML 1.1 resolves a bare `on`
key to the boolean true, so the declaration would arrive as the key `true`,
bind to nothing and generate a check with no property to agree on. That
spelling is refused by name on the raw tree, as `lifecycle`'s `on` already is.

Refused at parse, each because the declaration could not mean anything: not
exactly two relations, the same relation twice, a relation or an `onProperty` a
target does not declare, a `status:` gate (two relations either agree or they
do not, from the first save), an unknown `whenNull`, a terminal an equality is
not exact on (a decimal, a double, a date), and two terminals of DIFFERENT
types, where the boxed comparison is silently always false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gree

# Conflicts:
#	tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java
@delchev
delchev merged commit d554f0d into master Sep 17, 2026
10 checks passed
@delchev
delchev deleted the issue-7409-checks-agree branch September 17, 2026 12:51
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.

intent: checks kind agree - two relations of a junction row must agree on a shared target (Customer, Currency, Company)

1 participant