feat(intent): a child collection can outlive its master's lock - #6700
Merged
Conversation
An issued invoice could not be settled by hand. `immutableWhen` on the master
put the whole document page into read-only mode, and the generated UI extended
that lock over every CHILD PANEL - so the Add button and the row actions on
"Sales Invoice Customer Payments" existed only while the invoice was DRAFT.
Manual allocation was therefore unavailable in exactly the state where
allocations mean something, and a payment record has no allocation screen of
its own, so there was no other route.
The UI was enforcing a rule the model never declared. Verified against a live
instance: with the invoice ISSUED (`/{id}/mutable` false, a header PUT 409),
POSTing an allocation to the child controller returns 200, the Paid/Balance
roll-up recomputes (0 -> 20, balance 80) and the status advances to PARTIAL.
The child is a different entity with its own controller and no immutability of
its own; only the affordance was missing.
`locksWithMaster: false` on a composition child says so in the model: the
master's lock covers the document's own content, this collection keeps its
user-write affordances. Settlement is a different lifecycle from content - an
issued invoice's lines are frozen, money keeps arriving against it for months.
Parser validates both halves rather than ignoring a misplaced flag: it must be
a composition child, and its master must actually lock. An inert declaration
reads exactly like a working one, and the author would only discover it when
the affordance was still missing in production.
Emitted (and consumed) only when false, so every existing model regenerates
byte-identically. Relaxes the three gates on the document view and the same
three on the manage form view; a document's own line items are unaffected -
they render in the items pane, not a child panel, and stay locked.
Tests: EdmIntentGeneratorTest asserts the marker lands on the child and on
neither the master nor the line items; IntentParserTest covers both rejections
and the valid shape. Full engine-intent suite 148 green. The registry emission
was rendered through a real Velocity engine both ways (absent -> nothing,
false -> `locksWithMaster: false`).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Problem
An issued invoice cannot be settled by hand.
immutableWhenon the master puts the document page into read-only mode, and the generated UI extends that lock over every child panel — so the Add button and the row actions on Sales Invoice Customer Payments exist only while the invoice is DRAFT. Manual allocation is unavailable in exactly the state where allocations mean something, and a payment record has no allocation screen of its own, so there is no other route.The UI was enforcing a rule the model never declared
Verified against a live instance, with the invoice ISSUED:
GET /{id}/mutableon the master{"mutable": false}PUTthe master headerPOSTan allocation to the child controllerPaid 0 → 20,Balance 80, status → PARTIALThe child is a different entity, with its own controller and no immutability of its own. The server always permitted these writes — only the affordance was missing.
Change
locksWithMaster: falseon a composition child states it in the model:Settlement is a different lifecycle from content: an issued invoice's lines are frozen, money keeps arriving against it for months.
The parser validates both halves rather than ignoring a misplaced flag — it must be a composition child, and its master must actually lock. An inert declaration reads exactly like a working one, and the author would otherwise only discover it when the affordance was still missing in production.
Emitted (and consumed) only when false, so every existing model regenerates byte-identically. Relaxes the three gates on the document view and the same three on the manage form view. A document's own line items are unaffected — they render in the items pane, not a child panel, and stay locked.
Tests
EdmIntentGeneratorTest— the marker lands on the child, and on neither the master nor the line items.IntentParserTest— both rejections (not a composition child; master never locks) and the valid shape.engine-intentsuite: 148 green.false→locksWithMaster: false.🤖 Generated with Claude Code