Found while implementing #10946, which batched the PACKAGE door's boot seed (bootstrapDeclaredPermissions) and the position binder. The ENVIRONMENT door's boot reconciler has the same shape and was explicitly out of that card's scope.
The shape
packages/plugins/plugin-security/src/permission-set-projection.ts, in reconcilePermissionSetProjection:
- the overlay listing is already batched (one
tryFind(ql, 'sys_metadata', { type, state: 'active' }, 1000) per type) — that half is fine; - but the projection is then a per-name loop:
for (const name of overlayNames) { await projectPermissionMutation(...) }. Each iteration runs upsertEnvPermissionSet, which issues its own tryFind(ql, 'sys_permission_set', { name: ps.name }, 1) (line ~429) and then an UPDATE (line ~463) that fires whether or not the row already matches; - the second pass (
const records = await tryFind(ql, 'sys_permission_set', {}, 1000)) then walks every env-authored record with a per-row protocol.getMetaItemLayered call.
So the reconciler's cost grows with the number of environment-scope permission overlays, on every boot, and the write half changes nothing in the steady state.
What is and is not established
Established by reading the code: the loop shape and the unconditional UPDATE. Not established: the slope. I did not measure this axis — the hosted rig (bootstrap-curve.mjs) lives in objectstack-ai/cloud, outside the session that found this, and its axes are permission sets / positions / objects, not env overlays. Nor did I count how many overlays a typical hosted environment carries; on an environment with none, this loop costs nothing beyond its two batched reads, which may well be the common case. That question should be answered before the card is prioritised.
Why file it anyway
upsertEnvPermissionSet already has the exact comparison it needs sitting in the same file — recordDiffersFromBody(row, body), which the third branch of this very function (line ~1090) already trusts to decide whether a record drifted. The write-skip half is therefore close to free here, independent of whether the read is worth batching.
⚠️ The reason #10946 did not fold this in: the env door stamps customized on package-owned rows, and that flag is not part of what recordDiffersFromBody compares. A naive "skip when the body matches" would stop maintaining customized, which the Setup list badges on and the reset action reads. Any fix has to compare the customized patch too, or write it separately — that is a real design step, not a mechanical copy of #10946, which is why it is a card rather than a drive-by.
Refs #10946 · ADR-0094 · objectstack-ai/cloud#1555
Found while implementing #10946, which batched the PACKAGE door's boot seed (
bootstrapDeclaredPermissions) and the position binder. The ENVIRONMENT door's boot reconciler has the same shape and was explicitly out of that card's scope.The shape
packages/plugins/plugin-security/src/permission-set-projection.ts, inreconcilePermissionSetProjection:tryFind(ql, 'sys_metadata', { type, state: 'active' }, 1000)per type) — that half is fine;for (const name of overlayNames) { await projectPermissionMutation(...) }. Each iteration runsupsertEnvPermissionSet, which issues its owntryFind(ql, 'sys_permission_set', { name: ps.name }, 1)(line ~429) and then anUPDATE(line ~463) that fires whether or not the row already matches;const records = await tryFind(ql, 'sys_permission_set', {}, 1000)) then walks every env-authored record with a per-rowprotocol.getMetaItemLayeredcall.So the reconciler's cost grows with the number of environment-scope
permissionoverlays, on every boot, and the write half changes nothing in the steady state.What is and is not established
Established by reading the code: the loop shape and the unconditional
UPDATE. Not established: the slope. I did not measure this axis — the hosted rig (bootstrap-curve.mjs) lives inobjectstack-ai/cloud, outside the session that found this, and its axes are permission sets / positions / objects, not env overlays. Nor did I count how many overlays a typical hosted environment carries; on an environment with none, this loop costs nothing beyond its two batched reads, which may well be the common case. That question should be answered before the card is prioritised.Why file it anyway
upsertEnvPermissionSetalready has the exact comparison it needs sitting in the same file —recordDiffersFromBody(row, body), which the third branch of this very function (line ~1090) already trusts to decide whether a record drifted. The write-skip half is therefore close to free here, independent of whether the read is worth batching.customizedon package-owned rows, and that flag is not part of whatrecordDiffersFromBodycompares. A naive "skip when the body matches" would stop maintainingcustomized, which the Setup list badges on and the reset action reads. Any fix has to compare thecustomizedpatch too, or write it separately — that is a real design step, not a mechanical copy of #10946, which is why it is a card rather than a drive-by.Refs #10946 · ADR-0094 ·
objectstack-ai/cloud#1555