What
PermissionMatrixEditPage.doSave, under the package door, is documented to merge
the edited slice back onto a fresh read so that other packages' contributions
survive. Its own comment states the guarantee:
Package scope: merge only this package's slice back onto a fresh read of the
record so rows contributed by other packages survive byte-for-byte (ADR-0086 P0).
That fresh read is client.layered(...).catch(() => null). When it fails, the
guarantee inverts: the merge base falls back to payload — the editor's own
draft, which the load path already sliced down to this package's objects — so
mergePermissionSlice has no out-of-scope rows left to copy and the save PUTs a
record with the other packages' rows deleted. Success, 200, no error, no warning.
const fresh = await client.layered(type, payload.name).catch(() => null);
const base = (fresh?.effective ?? payload); // THE INVERSION
toSave = mergePermissionSlice(base, payload, scope);
Measured, not reasoned
One-shot probe driving the real PermissionMatrixEditPage (deleted after the run;
it was not landed). Published record carries a_account plus b_order (another
package's contribution) and the field permission b_order.total. The package door
is opened as app.a, whose object list is a_account only. layered resolves for
the load and rejects for the save-time re-read. One ordinary edit, then Save:
PROBE layeredCalls = 2
PROBE saved.objects keys = ["a_account"]
PROBE saved.fields keys = []
PROBE b_order present = false
b_order and b_order.total are gone from the body actually handed to
client.save. On the success arm the same scenario keeps them — that arm is pinned
today by PermissionMatrixEditor.packageDoorFacets.test.tsx. Only the failure arm
is unpinned, and only the failure arm loses rows.
Why it is worth a card
This is the same silent-discard class as objectui#4302 (an authored RLS policy
dropped with a 200), which is closed and does not cover this arm — its pins all
run with layered resolving. The blast radius here is larger than #4302's: #4302
lost the author's own unsaved edit, this loses another package's already-published
grants, and permission rows are a security surface.
⚠️ Not measured, and stated rather than assumed: how often layered actually
rejects in production. The arm is reachable by construction and the probe reaches
it, but no field incident is attached to this card.
Suggested shape, not a decision
The cheapest correct behaviour is probably to refuse the save when the re-read
fails, rather than proceed on a base that cannot honour the guarantee — a failed
re-read means the information needed to preserve out-of-scope rows is simply not in
hand. Whether that is a hard error, a retry, or a narrowed save is a product call
and is deliberately left open here.
⛔ Note that "merge from the unsliced draft instead" is not a fix: the load path
slices deliberately (ADR-0086 P0 keeps other packages' rows off-screen), so the
unsliced body is not available at that point either.
Provenance
Found while working objectui#8181 (PR #9419), whose pin harness exercises this exact
arm for a different reason (read decorations). Fenced out of that PR rather than
fixed there: different defect class, and #8181's fence is the read-decoration strip.
Deduped against the 63 open-and-closed issues carrying package: app-shell —
mergePermissionSlice, ADR-0086, and byte-for-byte row-preservation phrasings
return zero hits; the control term draft-envelope returns objectui#8181, so the
grep shape does reach the population.
Generated by Claude Code
What
PermissionMatrixEditPage.doSave, under the package door, is documented to mergethe edited slice back onto a fresh read so that other packages' contributions
survive. Its own comment states the guarantee:
That fresh read is
client.layered(...).catch(() => null). When it fails, theguarantee inverts: the merge base falls back to
payload— the editor's owndraft, which the load path already sliced down to this package's objects — so
mergePermissionSlicehas no out-of-scope rows left to copy and the save PUTs arecord with the other packages' rows deleted. Success, 200, no error, no warning.
Measured, not reasoned
One-shot probe driving the real
PermissionMatrixEditPage(deleted after the run;it was not landed). Published record carries
a_accountplusb_order(anotherpackage's contribution) and the field permission
b_order.total. The package dooris opened as
app.a, whose object list isa_accountonly.layeredresolves forthe load and rejects for the save-time re-read. One ordinary edit, then Save:
b_orderandb_order.totalare gone from the body actually handed toclient.save. On the success arm the same scenario keeps them — that arm is pinnedtoday by
PermissionMatrixEditor.packageDoorFacets.test.tsx. Only the failure armis unpinned, and only the failure arm loses rows.
Why it is worth a card
This is the same silent-discard class as objectui#4302 (an authored RLS policy
dropped with a 200), which is closed and does not cover this arm — its pins all
run with
layeredresolving. The blast radius here is larger than #4302's: #4302lost the author's own unsaved edit, this loses another package's already-published
grants, and permission rows are a security surface.
layeredactuallyrejects in production. The arm is reachable by construction and the probe reaches
it, but no field incident is attached to this card.
Suggested shape, not a decision
The cheapest correct behaviour is probably to refuse the save when the re-read
fails, rather than proceed on a base that cannot honour the guarantee — a failed
re-read means the information needed to preserve out-of-scope rows is simply not in
hand. Whether that is a hard error, a retry, or a narrowed save is a product call
and is deliberately left open here.
⛔ Note that "merge from the unsliced draft instead" is not a fix: the load path
slices deliberately (ADR-0086 P0 keeps other packages' rows off-screen), so the
unsliced body is not available at that point either.
Provenance
Found while working objectui#8181 (PR #9419), whose pin harness exercises this exact
arm for a different reason (read decorations). Fenced out of that PR rather than
fixed there: different defect class, and #8181's fence is the read-decoration strip.
Deduped against the 63 open-and-closed issues carrying
package: app-shell—mergePermissionSlice,ADR-0086, and byte-for-byte row-preservation phrasingsreturn zero hits; the control term
draft-envelopereturns objectui#8181, so thegrep shape does reach the population.
Generated by Claude Code