Skip to content

intent: a composition child can be personalReadOnly independently of its writable parent (#7340) - #7354

Open
delchev wants to merge 1 commit into
masterfrom
issue-7340-child-personal-readonly
Open

intent: a composition child can be personalReadOnly independently of its writable parent (#7340)#7354
delchev wants to merge 1 commit into
masterfrom
issue-7340-child-personal-readonly

Conversation

@delchev

@delchev delchev commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

The cause

personalReadOnly was declarable only alongside personal: true, and a composition child may not carry personal: at all - it inherits the scope through its parent. So a child's personal surface was writable exactly when its parent's was.

That is the wrong coupling whenever a person authors a header whose lines an engine writes. base-vacations files the leave request itself, so VacationRequest.Employee is personal: true and the surface has to be writable; VacationDay rows are created ONLY by VacationConsumeDelegate on approval, and each one charges a VacationEntitlement. The generated VacationDayMyController therefore accepted a POST from the employee's own DRAFT request naming any Entitlement id in the tenant - a colleague's leave account included - and the roll-up dutifully recorded the charge. The personal document page showed it as an Add button and a Fill Month action on the items panel.

No other key reaches it:

  • personalReadOnly on the parent closes the header the employee must author.
  • sensitive: true hides values on read; the write is still accepted.
  • immutableWhen does propagate to the child (requireMasterMutable), but only for the statuses the parent declares final - and a DRAFT parent is mutable by definition, which is precisely the window this lives in.

The shape recurs wherever a person authors a header and the engine writes the lines: an expense claim whose reimbursement lines a delegate computes, an order whose allocation rows a settlement writes.

The change

The key is now declarable on the child's composition relation - the edge the scope actually travels:

  - name: VacationDay
    relations:
      - { name: Request, kind: manyToOne, to: VacationRequest, composition: true, required: true,
          personalReadOnly: true }

Read as: the scope still comes from the parent; the writes do not. <Child>MyController's create / update / delete answer 403 and its personal pages render no write affordance, while the parent's own personal surface keeps its header Save/Delete. The power controller is untouched, and so is every engine writer, which goes through the repository.

Two derivations carry it to the pages, both in ModelParameterProcessor rather than emitted into the model, so a hand-authored .edm carrying the child's attribute gets the same pages:

  • inheritPersonalScope ORs the child's own relationshipPersonalReadOnly into its personalReadOnly.
  • The items panel lives on the master's document page, so the master's own flag could never express this: the master gets a derived documentItemsReadOnly that the document template gates Add / Fill Month / the per-row Delete / the item dialog's Save on. Each runtime child panel carries its own readOnly, so a see-only non-item child loses its Add too.

Refused at parse wherever the key would be carried nowhere - a dropped access declaration reads as a grant: on a relation that is neither personal: nor a composition, on a second composition (only the first is the ownership edge, mirroring whenMasterDeleted), and on a child whose master has no personal surface to inherit. A model that does not use the key generates byte-identically.

Verified

  • engine-intent + ide-template unit suites green (1243 tests), including the new InheritedPersonalReadOnlyIntentTest (6 cases), the EDM emission test and three ModelParameterProcessorTest derivation cases.
  • IntentEmissionCoverageIT - through the real templates, javac'd and published: a new writable Timesheet document master whose TimesheetLine (items) and TimesheetNote (panel) children declare the key. Asserts the child's controller 403s and emits no repository.save, the master's header keeps Save/Delete, the items Add / per-row Delete / dialog Save are gone while a line still opens for reading, the panel descriptor carries readOnly: true, and a writable child's panel still carries readOnly: false.
  • ModelGenerationIT green.
  • mvn formatter:validate green with the formatter cache wiped; the release profile's javadoc builds clean on both changed modules.

Not verified: no browser run - the UI half is asserted on the generated markup and page JS.

Docs

  • components/engine/engine-intent/CLAUDE.md and the intent assistant guide updated in this PR.
  • Spec proposal: IntentFile/intent-specification (proposal 0018-child-scoped-read-only.md).
  • Help portal: dirigible-io/dirigible-io.github.io.

Fixes #7340

🤖 Generated with Claude Code

…its writable parent (#7340)

`personalReadOnly` was declarable only alongside `personal: true`, and a
composition child may not carry `personal:` at all - it inherits the scope
through its parent. So a child's personal surface was writable exactly when its
parent's was, which is the wrong coupling whenever a person authors a header
whose lines an engine writes: base-vacations files the leave request itself (so
the surface must be writable) while VacationDay rows are written only by
VacationConsumeDelegate, each one charging a VacationEntitlement - and the
generated VacationDayMyController therefore accepted a POST from the employee's
own DRAFT naming any entitlement in the tenant, a colleague's account included,
with the roll-up dutifully recording the charge. The page showed it as an Add
button and a Fill Month action on the items panel.

No other key reaches it: personalReadOnly on the parent closes the header the
employee must author, `sensitive:` hides the value and still takes the write,
and `immutableWhen` propagates to the child only for the statuses the parent
declares final - a DRAFT parent is mutable by definition, which is precisely the
window the abuse lives in.

The key is now declarable on the CHILD's composition relation - the edge the
scope actually travels - reading as: the scope still comes from the parent, the
writes do not. The child's MyController answers 403 on create/update/delete and
its personal pages render no write affordance; the parent's own personal surface
keeps its header Save/Delete. Because the items panel lives on the MASTER's
document page, the master carries a derived documentItemsReadOnly the document
template gates Add / Fill Month / row delete / the item dialog's Save on, and
each runtime child panel carries its own readOnly so its Add is dropped too.
Deriving both in ModelParameterProcessor rather than emitting them means a
hand-authored .edm carrying the child's attribute gets the same pages.

Refused at parse wherever it would be carried nowhere - a dropped access
declaration reads as a grant: on a relation that is neither personal nor a
composition, on a second composition (only the first is the ownership edge), and
on a child whose master has no personal surface to inherit.

Verified: engine-intent + ide-template unit suites (1243 tests), a new
InheritedPersonalReadOnlyIntentTest, the EDM and processor derivation tests, and
- through the real templates, compiled and published - IntentEmissionCoverageIT
(a new writable Timesheet master whose line and note children are see-only) plus
ModelGenerationIT. formatter:validate green with the cache wiped; the javadoc
release profile builds clean on both modules.

Fixes #7340

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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