intent: a checks kind for two relations of a junction row that must agree on a shared target (#7409) - #7418
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
A junction/allocation entity links two records that both point at the same third thing - the same
Customer, the sameCurrency, the sameCompany, the sameEmployee. Nochecks:kind could say "these two relations must agree on X":comparerelates two values of ONE row, andrequiredWhen/forbidWhenrelate a child to its OWN parent. Nothing compared two DIFFERENT relations' targets.So a EUR
CustomerPaymentwas allocated against a USDSalesInvoice(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-writtencalculatedActionOnCreate/OnUpdateguard class whose entire content was a rule the intent should be able to state.The change
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.<relation>.<onProperty>through the sameResolvePathSupportwalker 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 itsuses:owner like any other hop.validate()of all three surfaces' controllers (EntityController,EntityMyController,EntityPartnerController) with 400 and the authored message - the same placeexactlyOneand an ungatedcomparerefuse - so it takes nostatus:gate.whenNull:defaults toskip(a row not yet carrying both values has nothing to disagree about, and requiredness is the relation's own declaration);refuserejects the write instead.One deviation from the proposal: the key is
onProperty, notonYAML 1.1 resolves a bare
onkey to the booleantrue, so anon: Customerdeclaration arrives as the keytrue, binds to nothing, and would generate a check with no property to agree on - the traplifecycle:already documents. Since the issue proposeson:, authors and the assistant will write it, so that spelling is refused by name on the raw tree (rejectCheckOn) pointing atonProperty, 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
onPropertya target does not declare (the walker's own message, so a cross-model target reads); astatus:gate; an unknownwhenNull; a terminal an equality is not exact on (adecimal, adouble, adate- the same line awhencondition draws); and two terminals of DIFFERENT declared types, where the boxed comparison is silently always false.An entity declaring no
agreecheck generates byte-identically.Verified
IntentParserTest.agreeChecksParseAndValidate- the shape and every refusal above, incl. theon: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 BOTHwhenNullreadings; 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).engine-intent(1317) andide-template(169) green;mvn formatter:validategreen with the cache wiped;-P releasejavadoc build green on both modules.Not verified: no browser-level check - an
agreerefusal 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