Skip to content

feat(intent): a child collection can outlive its master's lock - #6700

Merged
delchev merged 1 commit into
masterfrom
feat/child-collection-not-locked-by-master
Aug 13, 2026
Merged

feat(intent): a child collection can outlive its master's lock#6700
delchev merged 1 commit into
masterfrom
feat/child-collection-not-locked-by-master

Conversation

@delchev

@delchev delchev commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Problem

An issued invoice cannot be settled by hand.

immutableWhen on 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:

check result
GET /{id}/mutable on the master {"mutable": false}
PUT the master header 409
POST an allocation to the child controller 200
invoice after it Paid 0 → 20, Balance 80, status → PARTIAL

The 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: false on a composition child states it in the model:

- name: SalesInvoiceCustomerPayment
  locksWithMaster: false      # settlement is not part of the frozen document

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.
  • Full engine-intent suite: 148 green.
  • The registry emission rendered through a real Velocity engine both ways: absent → nothing, falselocksWithMaster: false.

🤖 Generated with Claude Code

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>
@delchev
delchev merged commit d363ac4 into master Aug 13, 2026
10 checks passed
@delchev
delchev deleted the feat/child-collection-not-locked-by-master branch August 13, 2026 07:56
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.

1 participant