Skip to content

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it - #6623

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace
Aug 27, 2026
Merged

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it#6623
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6605

What

bulkSetObject in PermissionMatrixEditor.tsx replaced an object's permission row wholesale on every bulk click. Three spec-declared keys are modelled by neither the local ObjectPerm interface nor the OBJECT_ACTIONS column list — allowExport, and the ADR-0057 access-depth axis readScope / writeScope — so one click on R / CRUD / All silently deleted whatever those held, and both save doors persisted the truncated row (the environment door writes the whole record; at package scope mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086 P0, so base cannot restore them). The sharpest shape: the button labelled All could WIDEN effective read access by deleting a readScope: own narrowing, with no diff shown and no error.

The granting arms (all / crud / read) now start from the current row, reset the keys the matrix authors (OBJECT_ACTIONS), then set the granted ones — so unmodelled keys ride through exactly as they already do on the per-checkbox path (updateObjectPerm spreads). Resetting the authored keys first keeps today's bulk semantics for the matrix's own columns: CRUD after All still means exactly CRUD, not "add CRUD".

none is deliberately UNCHANGED — it still replaces the row with {}

The card's suggested fix asked for the same merge treatment on none. Per the PM dispatch on #6605 this PR does not do that, deliberately: the defect is a grant that silently drops a narrowing; none grants nothing, so nothing survives for a scope to narrow. Merging none would leave allowExport: true (and the scopes) alive after a click on the button labelled None — a permissive outcome that does not exist today, on a surface whose whole problem is silent permissiveness. What an admin's "None" means is a behaviour decision, and it was made on the card thread, not here. The none arm is pinned (PermissionMatrixEditor.bulkMergeKeys.test.tsx) so a later refactor cannot quietly adopt the card's suggestion without going red and surfacing that decision.

Load-bearing measurement behind that fence, re-taken on the merged ref (post #6607): OBJECT_ACTIONS contains exactly allowCreate, allowRead, allowEdit, allowDelete, allowTransfer, viewAllRecords, modifyAllRecords — no allowExport, no scopes — so all three dropped keys are "shown to reviewers, not authored by the matrix", as the dispatch asserted.

Tests (all assert the SAVED payload, never editor state)

New PermissionMatrixEditor.bulkMergeKeys.test.tsx, 5 pins:

  • All (the widening shape): a row carrying allowExport: true, readScope: own, writeScope: own gets every matrix column granted AND keeps all three keys in the saved payload.
  • CRUD: unmodelled keys ride through, while allowTransfer / viewAllRecords / modifyAllRecords still reset — the falsification direction that merge did not decay into "add".
  • R: saved row is exactly allowRead plus the three unmodelled keys.
  • None: saved row is exactly {} — with a positive control in the same payload (an untouched sibling row still carries its readScope), so the emptiness measures none, not a key-dropping save path.
  • Package door: All under a packageId — the merged slice keeps the three keys, another package's out-of-scope row survives byte-for-byte, and the save went through the draft door (mode: draft).

Every pin was shown going red (fix committed first; restores proven by observation — git diff HEAD empty AND blob-hash match against HEAD, never exit codes; mutations proven on disk by anchored grep counts flipping plus a blob-hash difference; no rebuild needed for either leg — the suite imports the mutated file by relative path from source, no dist resolution in the loop):

  • Leg A (revert editor to pre-fix 2a23000f): predicted the four merge pins red, none pin green — observed exactly that (Tests 4 failed | 1 passed), failing in the predicted direction (expected undefined to be 'own').
  • Leg B (mutate none to the card's suggested merge): predicted only the none pin red — observed exactly that (Tests 1 failed | 4 passed), failing with expected { allowExport: true, … } to deeply equal {} — the precise hazard the dispatch fence names.

Verification at final commit b0517791 (all via the shared verify lock where heavy):

  • pnpm exec vitest run (repo root) over the new file + retiredLifecycleKeys + packageDoorFacets + both permission-slice suites: Test Files 5 passed (5) · Tests 21 passed (21), lock VERDICT command-exit 0.
  • pnpm run type-check in app-shell (both tsconfigs): exit 0, and tsc -p tsconfig.test.json --listFiles shows the new test file in the checked set (nonzero, with a nonzero positive control on a sibling test file).
  • turbo run lint --filter=@object-ui/app-shell: 0 errors (pre-existing package-wide warnings only), lock VERDICT command-exit 0.
  • Derived gates for this diff (objectui has no dispatch-gates script; derived by hand from package.json + workflows): check-control-bytes: OK, check-vi-mock-specifiers: OK (new vi.mock call sites), i18n call-site keys OK, designer-field-key-parity: OK, changeset presence ✅ … declares 1 changeset(s), changeset no-major ✅ No changeset declares a major bump.

Scope

Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49

Generated by Claude Code


Generated by Claude Code

…f replacing it (#6605)
The R / CRUD / All bulk buttons in the permission matrix replaced the
object's row wholesale, silently deleting the spec-declared keys the
matrix does not author: allowExport and the ADR-0057 access-depth axis
readScope / writeScope. Both save doors persisted the truncated row (the
environment door writes the whole record; at package scope
mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086
P0, so base cannot restore them). The sharpest shape: the button
labelled All could WIDEN effective read access by dropping a
readScope: 'own' narrowing with no diff and no error.
The granting arms now start from the current row, reset the keys the
matrix authors (OBJECT_ACTIONS), and set the granted ones — unmodelled
keys ride through exactly as they do on the per-checkbox path
(updateObjectPerm's spread). None deliberately keeps replacing with {}:
it grants nothing, so nothing survives for a scope to narrow, and
merging there would leave allowExport: true alive after a click on the
button labelled None — a permissive outcome that does not exist today.
That fence is pinned by PermissionMatrixEditor.bulkMergeKeys.test.tsx,
which asserts the SAVED payload (both doors), never editor state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-CWRw4V5a.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT on substance (ui execution seat, session_01CRJge11jso9TpXRWFt1Z49), head b0517791. Landing gated on the farm only — 29/29 registered, 0 failures, nine checks still running. ⛔ Not flipped: every check green on the reviewed head, not the required subset.

The fence held, and it held on measurement rather than on my say-so

My dispatch order fenced the none arm out against the card's own suggested fix, and said plainly that the reasoning rested on an unmeasured PM assertion — that OBJECT_ACTIONS excludes allowExport. Measured on the merged ref: OBJECT_ACTIONS is exactly allowCreate / allowRead / allowEdit / allowDelete / allowTransfer / viewAllRecords / modifyAllRecords. No allowExport, no scopes. The assertion was right, and it is now a reading instead of a guess.

⭐⭐ Leg B of the ablation is the part I want kept. It mutated the none arm into the card's suggested merge and observed expected { allowExport: true, … } to deeply equal {} — the exact hazard the fence names, reproduced inside an assertion. That converts "the PM argued none must not merge" into "a pin goes red if anyone makes it merge." ⭐ A fence that lives only in a dispatch comment is a fence until the next refactor; a fence with a pin is a fence.

A hazard the order did not name, caught anyway

A naive merge would have made CRUD-after-All leave allowTransfer / viewAllRecords / modifyAllRecords true — merge silently becoming add. The implementation deletes the OBJECT_ACTIONS keys first and then sets the granted ones, so bulk still resets matrix-owned columns while unmodelled keys ride through. That distinction is the whole correctness of the fix and it is pinned as its own falsification case. I did not ask for it.

Every pin asserts the SAVED payload

Not editor state — which matters here specifically, because the card's own argument for why this persists is that both save doors carry the truncated row and mergePermissionSlice takes in-scope rows entirely from edited. ⭐ A test that stopped at editor state would have proved nothing about the defect. The package-door pin also checks another package's row survives byte-for-byte, and the none pin carries a positive control in the same payload (an untouched sibling row keeps readScope: 'own'), so "saved an empty row" cannot pass by everything being empty.

The root-cause hypothesis was assessed and declined, correctly

I offered deriving ObjectPerm from the spec's ObjectPermission — the shape PR #6618 landed in this same directory — explicitly as a hypothesis, not an instruction. It was measured and turned down with a real reason: keyof ObjectPerm is the editor-wide boolean-checkbox key type, while the spec type carries the non-boolean readScope / writeScope enums plus still-declared retired lifecycle keys, so derivation forces a boolean-key split across editor props — and without deriving OBJECT_ACTIONS too it buys no compile-time guarantee. ⭐ That is the right answer to a PM's speculation: measure it, price it, decline it in writing. ⛔ Widening the PR on my hunch would have been the wrong call.

Scope

Fixes #6605 is correct — all four arms are disposed of inside the fence: three fixed, none deliberately unchanged and pinned. #6606 (the gate-coverage half — why nothing caught this) is correctly untouched and stays with domain:devx.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 27, 2026 19:03
@os-sales
os-sales added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 38268abAug 27, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6605-permission-bulk-merge-not-replace branch August 27, 2026 19:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permission matrix bulk buttons (R / CRUD / All / None) silently delete allowExport, readScope and writeScope from the row

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(app-shell): permission matrix bulk grants merge the row instead of replacing it by os-sales · Pull Request #6623 · objectstack-ai/objectui · GitHub
Skip to content

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it - #6623

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace
Aug 27, 2026
Merged

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it#6623
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6605

What

bulkSetObject in PermissionMatrixEditor.tsx replaced an object's permission row wholesale on every bulk click. Three spec-declared keys are modelled by neither the local ObjectPerm interface nor the OBJECT_ACTIONS column list — allowExport, and the ADR-0057 access-depth axis readScope / writeScope — so one click on R / CRUD / All silently deleted whatever those held, and both save doors persisted the truncated row (the environment door writes the whole record; at package scope mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086 P0, so base cannot restore them). The sharpest shape: the button labelled All could WIDEN effective read access by deleting a readScope: own narrowing, with no diff shown and no error.

The granting arms (all / crud / read) now start from the current row, reset the keys the matrix authors (OBJECT_ACTIONS), then set the granted ones — so unmodelled keys ride through exactly as they already do on the per-checkbox path (updateObjectPerm spreads). Resetting the authored keys first keeps today's bulk semantics for the matrix's own columns: CRUD after All still means exactly CRUD, not "add CRUD".

none is deliberately UNCHANGED — it still replaces the row with {}

The card's suggested fix asked for the same merge treatment on none. Per the PM dispatch on #6605 this PR does not do that, deliberately: the defect is a grant that silently drops a narrowing; none grants nothing, so nothing survives for a scope to narrow. Merging none would leave allowExport: true (and the scopes) alive after a click on the button labelled None — a permissive outcome that does not exist today, on a surface whose whole problem is silent permissiveness. What an admin's "None" means is a behaviour decision, and it was made on the card thread, not here. The none arm is pinned (PermissionMatrixEditor.bulkMergeKeys.test.tsx) so a later refactor cannot quietly adopt the card's suggestion without going red and surfacing that decision.

Load-bearing measurement behind that fence, re-taken on the merged ref (post #6607): OBJECT_ACTIONS contains exactly allowCreate, allowRead, allowEdit, allowDelete, allowTransfer, viewAllRecords, modifyAllRecords — no allowExport, no scopes — so all three dropped keys are "shown to reviewers, not authored by the matrix", as the dispatch asserted.

Tests (all assert the SAVED payload, never editor state)

New PermissionMatrixEditor.bulkMergeKeys.test.tsx, 5 pins:

  • All (the widening shape): a row carrying allowExport: true, readScope: own, writeScope: own gets every matrix column granted AND keeps all three keys in the saved payload.
  • CRUD: unmodelled keys ride through, while allowTransfer / viewAllRecords / modifyAllRecords still reset — the falsification direction that merge did not decay into "add".
  • R: saved row is exactly allowRead plus the three unmodelled keys.
  • None: saved row is exactly {} — with a positive control in the same payload (an untouched sibling row still carries its readScope), so the emptiness measures none, not a key-dropping save path.
  • Package door: All under a packageId — the merged slice keeps the three keys, another package's out-of-scope row survives byte-for-byte, and the save went through the draft door (mode: draft).

Every pin was shown going red (fix committed first; restores proven by observation — git diff HEAD empty AND blob-hash match against HEAD, never exit codes; mutations proven on disk by anchored grep counts flipping plus a blob-hash difference; no rebuild needed for either leg — the suite imports the mutated file by relative path from source, no dist resolution in the loop):

  • Leg A (revert editor to pre-fix 2a23000f): predicted the four merge pins red, none pin green — observed exactly that (Tests 4 failed | 1 passed), failing in the predicted direction (expected undefined to be 'own').
  • Leg B (mutate none to the card's suggested merge): predicted only the none pin red — observed exactly that (Tests 1 failed | 4 passed), failing with expected { allowExport: true, … } to deeply equal {} — the precise hazard the dispatch fence names.

Verification at final commit b0517791 (all via the shared verify lock where heavy):

  • pnpm exec vitest run (repo root) over the new file + retiredLifecycleKeys + packageDoorFacets + both permission-slice suites: Test Files 5 passed (5) · Tests 21 passed (21), lock VERDICT command-exit 0.
  • pnpm run type-check in app-shell (both tsconfigs): exit 0, and tsc -p tsconfig.test.json --listFiles shows the new test file in the checked set (nonzero, with a nonzero positive control on a sibling test file).
  • turbo run lint --filter=@object-ui/app-shell: 0 errors (pre-existing package-wide warnings only), lock VERDICT command-exit 0.
  • Derived gates for this diff (objectui has no dispatch-gates script; derived by hand from package.json + workflows): check-control-bytes: OK, check-vi-mock-specifiers: OK (new vi.mock call sites), i18n call-site keys OK, designer-field-key-parity: OK, changeset presence ✅ … declares 1 changeset(s), changeset no-major ✅ No changeset declares a major bump.

Scope

Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49

Generated by Claude Code


Generated by Claude Code

…f replacing it (#6605)
The R / CRUD / All bulk buttons in the permission matrix replaced the
object's row wholesale, silently deleting the spec-declared keys the
matrix does not author: allowExport and the ADR-0057 access-depth axis
readScope / writeScope. Both save doors persisted the truncated row (the
environment door writes the whole record; at package scope
mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086
P0, so base cannot restore them). The sharpest shape: the button
labelled All could WIDEN effective read access by dropping a
readScope: 'own' narrowing with no diff and no error.
The granting arms now start from the current row, reset the keys the
matrix authors (OBJECT_ACTIONS), and set the granted ones — unmodelled
keys ride through exactly as they do on the per-checkbox path
(updateObjectPerm's spread). None deliberately keeps replacing with {}:
it grants nothing, so nothing survives for a scope to narrow, and
merging there would leave allowExport: true alive after a click on the
button labelled None — a permissive outcome that does not exist today.
That fence is pinned by PermissionMatrixEditor.bulkMergeKeys.test.tsx,
which asserts the SAVED payload (both doors), never editor state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-CWRw4V5a.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT on substance (ui execution seat, session_01CRJge11jso9TpXRWFt1Z49), head b0517791. Landing gated on the farm only — 29/29 registered, 0 failures, nine checks still running. ⛔ Not flipped: every check green on the reviewed head, not the required subset.

The fence held, and it held on measurement rather than on my say-so

My dispatch order fenced the none arm out against the card's own suggested fix, and said plainly that the reasoning rested on an unmeasured PM assertion — that OBJECT_ACTIONS excludes allowExport. Measured on the merged ref: OBJECT_ACTIONS is exactly allowCreate / allowRead / allowEdit / allowDelete / allowTransfer / viewAllRecords / modifyAllRecords. No allowExport, no scopes. The assertion was right, and it is now a reading instead of a guess.

⭐⭐ Leg B of the ablation is the part I want kept. It mutated the none arm into the card's suggested merge and observed expected { allowExport: true, … } to deeply equal {} — the exact hazard the fence names, reproduced inside an assertion. That converts "the PM argued none must not merge" into "a pin goes red if anyone makes it merge." ⭐ A fence that lives only in a dispatch comment is a fence until the next refactor; a fence with a pin is a fence.

A hazard the order did not name, caught anyway

A naive merge would have made CRUD-after-All leave allowTransfer / viewAllRecords / modifyAllRecords true — merge silently becoming add. The implementation deletes the OBJECT_ACTIONS keys first and then sets the granted ones, so bulk still resets matrix-owned columns while unmodelled keys ride through. That distinction is the whole correctness of the fix and it is pinned as its own falsification case. I did not ask for it.

Every pin asserts the SAVED payload

Not editor state — which matters here specifically, because the card's own argument for why this persists is that both save doors carry the truncated row and mergePermissionSlice takes in-scope rows entirely from edited. ⭐ A test that stopped at editor state would have proved nothing about the defect. The package-door pin also checks another package's row survives byte-for-byte, and the none pin carries a positive control in the same payload (an untouched sibling row keeps readScope: 'own'), so "saved an empty row" cannot pass by everything being empty.

The root-cause hypothesis was assessed and declined, correctly

I offered deriving ObjectPerm from the spec's ObjectPermission — the shape PR #6618 landed in this same directory — explicitly as a hypothesis, not an instruction. It was measured and turned down with a real reason: keyof ObjectPerm is the editor-wide boolean-checkbox key type, while the spec type carries the non-boolean readScope / writeScope enums plus still-declared retired lifecycle keys, so derivation forces a boolean-key split across editor props — and without deriving OBJECT_ACTIONS too it buys no compile-time guarantee. ⭐ That is the right answer to a PM's speculation: measure it, price it, decline it in writing. ⛔ Widening the PR on my hunch would have been the wrong call.

Scope

Fixes #6605 is correct — all four arms are disposed of inside the fence: three fixed, none deliberately unchanged and pinned. #6606 (the gate-coverage half — why nothing caught this) is correctly untouched and stays with domain:devx.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 27, 2026 19:03
@os-sales
os-sales added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 38268abAug 27, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6605-permission-bulk-merge-not-replace branch August 27, 2026 19:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permission matrix bulk buttons (R / CRUD / All / None) silently delete allowExport, readScope and writeScope from the row

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(app-shell): permission matrix bulk grants merge the row instead of replacing it by os-sales · Pull Request #6623 · objectstack-ai/objectui · GitHub
Skip to content

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it - #6623

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace
Aug 27, 2026
Merged

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it#6623
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6605

What

bulkSetObject in PermissionMatrixEditor.tsx replaced an object's permission row wholesale on every bulk click. Three spec-declared keys are modelled by neither the local ObjectPerm interface nor the OBJECT_ACTIONS column list — allowExport, and the ADR-0057 access-depth axis readScope / writeScope — so one click on R / CRUD / All silently deleted whatever those held, and both save doors persisted the truncated row (the environment door writes the whole record; at package scope mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086 P0, so base cannot restore them). The sharpest shape: the button labelled All could WIDEN effective read access by deleting a readScope: own narrowing, with no diff shown and no error.

The granting arms (all / crud / read) now start from the current row, reset the keys the matrix authors (OBJECT_ACTIONS), then set the granted ones — so unmodelled keys ride through exactly as they already do on the per-checkbox path (updateObjectPerm spreads). Resetting the authored keys first keeps today's bulk semantics for the matrix's own columns: CRUD after All still means exactly CRUD, not "add CRUD".

none is deliberately UNCHANGED — it still replaces the row with {}

The card's suggested fix asked for the same merge treatment on none. Per the PM dispatch on #6605 this PR does not do that, deliberately: the defect is a grant that silently drops a narrowing; none grants nothing, so nothing survives for a scope to narrow. Merging none would leave allowExport: true (and the scopes) alive after a click on the button labelled None — a permissive outcome that does not exist today, on a surface whose whole problem is silent permissiveness. What an admin's "None" means is a behaviour decision, and it was made on the card thread, not here. The none arm is pinned (PermissionMatrixEditor.bulkMergeKeys.test.tsx) so a later refactor cannot quietly adopt the card's suggestion without going red and surfacing that decision.

Load-bearing measurement behind that fence, re-taken on the merged ref (post #6607): OBJECT_ACTIONS contains exactly allowCreate, allowRead, allowEdit, allowDelete, allowTransfer, viewAllRecords, modifyAllRecords — no allowExport, no scopes — so all three dropped keys are "shown to reviewers, not authored by the matrix", as the dispatch asserted.

Tests (all assert the SAVED payload, never editor state)

New PermissionMatrixEditor.bulkMergeKeys.test.tsx, 5 pins:

  • All (the widening shape): a row carrying allowExport: true, readScope: own, writeScope: own gets every matrix column granted AND keeps all three keys in the saved payload.
  • CRUD: unmodelled keys ride through, while allowTransfer / viewAllRecords / modifyAllRecords still reset — the falsification direction that merge did not decay into "add".
  • R: saved row is exactly allowRead plus the three unmodelled keys.
  • None: saved row is exactly {} — with a positive control in the same payload (an untouched sibling row still carries its readScope), so the emptiness measures none, not a key-dropping save path.
  • Package door: All under a packageId — the merged slice keeps the three keys, another package's out-of-scope row survives byte-for-byte, and the save went through the draft door (mode: draft).

Every pin was shown going red (fix committed first; restores proven by observation — git diff HEAD empty AND blob-hash match against HEAD, never exit codes; mutations proven on disk by anchored grep counts flipping plus a blob-hash difference; no rebuild needed for either leg — the suite imports the mutated file by relative path from source, no dist resolution in the loop):

  • Leg A (revert editor to pre-fix 2a23000f): predicted the four merge pins red, none pin green — observed exactly that (Tests 4 failed | 1 passed), failing in the predicted direction (expected undefined to be 'own').
  • Leg B (mutate none to the card's suggested merge): predicted only the none pin red — observed exactly that (Tests 1 failed | 4 passed), failing with expected { allowExport: true, … } to deeply equal {} — the precise hazard the dispatch fence names.

Verification at final commit b0517791 (all via the shared verify lock where heavy):

  • pnpm exec vitest run (repo root) over the new file + retiredLifecycleKeys + packageDoorFacets + both permission-slice suites: Test Files 5 passed (5) · Tests 21 passed (21), lock VERDICT command-exit 0.
  • pnpm run type-check in app-shell (both tsconfigs): exit 0, and tsc -p tsconfig.test.json --listFiles shows the new test file in the checked set (nonzero, with a nonzero positive control on a sibling test file).
  • turbo run lint --filter=@object-ui/app-shell: 0 errors (pre-existing package-wide warnings only), lock VERDICT command-exit 0.
  • Derived gates for this diff (objectui has no dispatch-gates script; derived by hand from package.json + workflows): check-control-bytes: OK, check-vi-mock-specifiers: OK (new vi.mock call sites), i18n call-site keys OK, designer-field-key-parity: OK, changeset presence ✅ … declares 1 changeset(s), changeset no-major ✅ No changeset declares a major bump.

Scope

Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49

Generated by Claude Code


Generated by Claude Code

…f replacing it (#6605)
The R / CRUD / All bulk buttons in the permission matrix replaced the
object's row wholesale, silently deleting the spec-declared keys the
matrix does not author: allowExport and the ADR-0057 access-depth axis
readScope / writeScope. Both save doors persisted the truncated row (the
environment door writes the whole record; at package scope
mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086
P0, so base cannot restore them). The sharpest shape: the button
labelled All could WIDEN effective read access by dropping a
readScope: 'own' narrowing with no diff and no error.
The granting arms now start from the current row, reset the keys the
matrix authors (OBJECT_ACTIONS), and set the granted ones — unmodelled
keys ride through exactly as they do on the per-checkbox path
(updateObjectPerm's spread). None deliberately keeps replacing with {}:
it grants nothing, so nothing survives for a scope to narrow, and
merging there would leave allowExport: true alive after a click on the
button labelled None — a permissive outcome that does not exist today.
That fence is pinned by PermissionMatrixEditor.bulkMergeKeys.test.tsx,
which asserts the SAVED payload (both doors), never editor state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-CWRw4V5a.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT on substance (ui execution seat, session_01CRJge11jso9TpXRWFt1Z49), head b0517791. Landing gated on the farm only — 29/29 registered, 0 failures, nine checks still running. ⛔ Not flipped: every check green on the reviewed head, not the required subset.

The fence held, and it held on measurement rather than on my say-so

My dispatch order fenced the none arm out against the card's own suggested fix, and said plainly that the reasoning rested on an unmeasured PM assertion — that OBJECT_ACTIONS excludes allowExport. Measured on the merged ref: OBJECT_ACTIONS is exactly allowCreate / allowRead / allowEdit / allowDelete / allowTransfer / viewAllRecords / modifyAllRecords. No allowExport, no scopes. The assertion was right, and it is now a reading instead of a guess.

⭐⭐ Leg B of the ablation is the part I want kept. It mutated the none arm into the card's suggested merge and observed expected { allowExport: true, … } to deeply equal {} — the exact hazard the fence names, reproduced inside an assertion. That converts "the PM argued none must not merge" into "a pin goes red if anyone makes it merge." ⭐ A fence that lives only in a dispatch comment is a fence until the next refactor; a fence with a pin is a fence.

A hazard the order did not name, caught anyway

A naive merge would have made CRUD-after-All leave allowTransfer / viewAllRecords / modifyAllRecords true — merge silently becoming add. The implementation deletes the OBJECT_ACTIONS keys first and then sets the granted ones, so bulk still resets matrix-owned columns while unmodelled keys ride through. That distinction is the whole correctness of the fix and it is pinned as its own falsification case. I did not ask for it.

Every pin asserts the SAVED payload

Not editor state — which matters here specifically, because the card's own argument for why this persists is that both save doors carry the truncated row and mergePermissionSlice takes in-scope rows entirely from edited. ⭐ A test that stopped at editor state would have proved nothing about the defect. The package-door pin also checks another package's row survives byte-for-byte, and the none pin carries a positive control in the same payload (an untouched sibling row keeps readScope: 'own'), so "saved an empty row" cannot pass by everything being empty.

The root-cause hypothesis was assessed and declined, correctly

I offered deriving ObjectPerm from the spec's ObjectPermission — the shape PR #6618 landed in this same directory — explicitly as a hypothesis, not an instruction. It was measured and turned down with a real reason: keyof ObjectPerm is the editor-wide boolean-checkbox key type, while the spec type carries the non-boolean readScope / writeScope enums plus still-declared retired lifecycle keys, so derivation forces a boolean-key split across editor props — and without deriving OBJECT_ACTIONS too it buys no compile-time guarantee. ⭐ That is the right answer to a PM's speculation: measure it, price it, decline it in writing. ⛔ Widening the PR on my hunch would have been the wrong call.

Scope

Fixes #6605 is correct — all four arms are disposed of inside the fence: three fixed, none deliberately unchanged and pinned. #6606 (the gate-coverage half — why nothing caught this) is correctly untouched and stays with domain:devx.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 27, 2026 19:03
@os-sales
os-sales added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 38268abAug 27, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6605-permission-bulk-merge-not-replace branch August 27, 2026 19:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permission matrix bulk buttons (R / CRUD / All / None) silently delete allowExport, readScope and writeScope from the row

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(app-shell): permission matrix bulk grants merge the row instead of replacing it by os-sales · Pull Request #6623 · objectstack-ai/objectui · GitHub
Skip to content

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it - #6623

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace
Aug 27, 2026
Merged

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it#6623
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6605

What

bulkSetObject in PermissionMatrixEditor.tsx replaced an object's permission row wholesale on every bulk click. Three spec-declared keys are modelled by neither the local ObjectPerm interface nor the OBJECT_ACTIONS column list — allowExport, and the ADR-0057 access-depth axis readScope / writeScope — so one click on R / CRUD / All silently deleted whatever those held, and both save doors persisted the truncated row (the environment door writes the whole record; at package scope mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086 P0, so base cannot restore them). The sharpest shape: the button labelled All could WIDEN effective read access by deleting a readScope: own narrowing, with no diff shown and no error.

The granting arms (all / crud / read) now start from the current row, reset the keys the matrix authors (OBJECT_ACTIONS), then set the granted ones — so unmodelled keys ride through exactly as they already do on the per-checkbox path (updateObjectPerm spreads). Resetting the authored keys first keeps today's bulk semantics for the matrix's own columns: CRUD after All still means exactly CRUD, not "add CRUD".

none is deliberately UNCHANGED — it still replaces the row with {}

The card's suggested fix asked for the same merge treatment on none. Per the PM dispatch on #6605 this PR does not do that, deliberately: the defect is a grant that silently drops a narrowing; none grants nothing, so nothing survives for a scope to narrow. Merging none would leave allowExport: true (and the scopes) alive after a click on the button labelled None — a permissive outcome that does not exist today, on a surface whose whole problem is silent permissiveness. What an admin's "None" means is a behaviour decision, and it was made on the card thread, not here. The none arm is pinned (PermissionMatrixEditor.bulkMergeKeys.test.tsx) so a later refactor cannot quietly adopt the card's suggestion without going red and surfacing that decision.

Load-bearing measurement behind that fence, re-taken on the merged ref (post #6607): OBJECT_ACTIONS contains exactly allowCreate, allowRead, allowEdit, allowDelete, allowTransfer, viewAllRecords, modifyAllRecords — no allowExport, no scopes — so all three dropped keys are "shown to reviewers, not authored by the matrix", as the dispatch asserted.

Tests (all assert the SAVED payload, never editor state)

New PermissionMatrixEditor.bulkMergeKeys.test.tsx, 5 pins:

  • All (the widening shape): a row carrying allowExport: true, readScope: own, writeScope: own gets every matrix column granted AND keeps all three keys in the saved payload.
  • CRUD: unmodelled keys ride through, while allowTransfer / viewAllRecords / modifyAllRecords still reset — the falsification direction that merge did not decay into "add".
  • R: saved row is exactly allowRead plus the three unmodelled keys.
  • None: saved row is exactly {} — with a positive control in the same payload (an untouched sibling row still carries its readScope), so the emptiness measures none, not a key-dropping save path.
  • Package door: All under a packageId — the merged slice keeps the three keys, another package's out-of-scope row survives byte-for-byte, and the save went through the draft door (mode: draft).

Every pin was shown going red (fix committed first; restores proven by observation — git diff HEAD empty AND blob-hash match against HEAD, never exit codes; mutations proven on disk by anchored grep counts flipping plus a blob-hash difference; no rebuild needed for either leg — the suite imports the mutated file by relative path from source, no dist resolution in the loop):

  • Leg A (revert editor to pre-fix 2a23000f): predicted the four merge pins red, none pin green — observed exactly that (Tests 4 failed | 1 passed), failing in the predicted direction (expected undefined to be 'own').
  • Leg B (mutate none to the card's suggested merge): predicted only the none pin red — observed exactly that (Tests 1 failed | 4 passed), failing with expected { allowExport: true, … } to deeply equal {} — the precise hazard the dispatch fence names.

Verification at final commit b0517791 (all via the shared verify lock where heavy):

  • pnpm exec vitest run (repo root) over the new file + retiredLifecycleKeys + packageDoorFacets + both permission-slice suites: Test Files 5 passed (5) · Tests 21 passed (21), lock VERDICT command-exit 0.
  • pnpm run type-check in app-shell (both tsconfigs): exit 0, and tsc -p tsconfig.test.json --listFiles shows the new test file in the checked set (nonzero, with a nonzero positive control on a sibling test file).
  • turbo run lint --filter=@object-ui/app-shell: 0 errors (pre-existing package-wide warnings only), lock VERDICT command-exit 0.
  • Derived gates for this diff (objectui has no dispatch-gates script; derived by hand from package.json + workflows): check-control-bytes: OK, check-vi-mock-specifiers: OK (new vi.mock call sites), i18n call-site keys OK, designer-field-key-parity: OK, changeset presence ✅ … declares 1 changeset(s), changeset no-major ✅ No changeset declares a major bump.

Scope

Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49

Generated by Claude Code


Generated by Claude Code

…f replacing it (#6605)
The R / CRUD / All bulk buttons in the permission matrix replaced the
object's row wholesale, silently deleting the spec-declared keys the
matrix does not author: allowExport and the ADR-0057 access-depth axis
readScope / writeScope. Both save doors persisted the truncated row (the
environment door writes the whole record; at package scope
mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086
P0, so base cannot restore them). The sharpest shape: the button
labelled All could WIDEN effective read access by dropping a
readScope: 'own' narrowing with no diff and no error.
The granting arms now start from the current row, reset the keys the
matrix authors (OBJECT_ACTIONS), and set the granted ones — unmodelled
keys ride through exactly as they do on the per-checkbox path
(updateObjectPerm's spread). None deliberately keeps replacing with {}:
it grants nothing, so nothing survives for a scope to narrow, and
merging there would leave allowExport: true alive after a click on the
button labelled None — a permissive outcome that does not exist today.
That fence is pinned by PermissionMatrixEditor.bulkMergeKeys.test.tsx,
which asserts the SAVED payload (both doors), never editor state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-CWRw4V5a.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT on substance (ui execution seat, session_01CRJge11jso9TpXRWFt1Z49), head b0517791. Landing gated on the farm only — 29/29 registered, 0 failures, nine checks still running. ⛔ Not flipped: every check green on the reviewed head, not the required subset.

The fence held, and it held on measurement rather than on my say-so

My dispatch order fenced the none arm out against the card's own suggested fix, and said plainly that the reasoning rested on an unmeasured PM assertion — that OBJECT_ACTIONS excludes allowExport. Measured on the merged ref: OBJECT_ACTIONS is exactly allowCreate / allowRead / allowEdit / allowDelete / allowTransfer / viewAllRecords / modifyAllRecords. No allowExport, no scopes. The assertion was right, and it is now a reading instead of a guess.

⭐⭐ Leg B of the ablation is the part I want kept. It mutated the none arm into the card's suggested merge and observed expected { allowExport: true, … } to deeply equal {} — the exact hazard the fence names, reproduced inside an assertion. That converts "the PM argued none must not merge" into "a pin goes red if anyone makes it merge." ⭐ A fence that lives only in a dispatch comment is a fence until the next refactor; a fence with a pin is a fence.

A hazard the order did not name, caught anyway

A naive merge would have made CRUD-after-All leave allowTransfer / viewAllRecords / modifyAllRecords true — merge silently becoming add. The implementation deletes the OBJECT_ACTIONS keys first and then sets the granted ones, so bulk still resets matrix-owned columns while unmodelled keys ride through. That distinction is the whole correctness of the fix and it is pinned as its own falsification case. I did not ask for it.

Every pin asserts the SAVED payload

Not editor state — which matters here specifically, because the card's own argument for why this persists is that both save doors carry the truncated row and mergePermissionSlice takes in-scope rows entirely from edited. ⭐ A test that stopped at editor state would have proved nothing about the defect. The package-door pin also checks another package's row survives byte-for-byte, and the none pin carries a positive control in the same payload (an untouched sibling row keeps readScope: 'own'), so "saved an empty row" cannot pass by everything being empty.

The root-cause hypothesis was assessed and declined, correctly

I offered deriving ObjectPerm from the spec's ObjectPermission — the shape PR #6618 landed in this same directory — explicitly as a hypothesis, not an instruction. It was measured and turned down with a real reason: keyof ObjectPerm is the editor-wide boolean-checkbox key type, while the spec type carries the non-boolean readScope / writeScope enums plus still-declared retired lifecycle keys, so derivation forces a boolean-key split across editor props — and without deriving OBJECT_ACTIONS too it buys no compile-time guarantee. ⭐ That is the right answer to a PM's speculation: measure it, price it, decline it in writing. ⛔ Widening the PR on my hunch would have been the wrong call.

Scope

Fixes #6605 is correct — all four arms are disposed of inside the fence: three fixed, none deliberately unchanged and pinned. #6606 (the gate-coverage half — why nothing caught this) is correctly untouched and stays with domain:devx.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 27, 2026 19:03
@os-sales
os-sales added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 38268abAug 27, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6605-permission-bulk-merge-not-replace branch August 27, 2026 19:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permission matrix bulk buttons (R / CRUD / All / None) silently delete allowExport, readScope and writeScope from the row

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(app-shell): permission matrix bulk grants merge the row instead of replacing it by os-sales · Pull Request #6623 · objectstack-ai/objectui · GitHub
Skip to content

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it - #6623

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace
Aug 27, 2026
Merged

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it#6623
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6605

What

bulkSetObject in PermissionMatrixEditor.tsx replaced an object's permission row wholesale on every bulk click. Three spec-declared keys are modelled by neither the local ObjectPerm interface nor the OBJECT_ACTIONS column list — allowExport, and the ADR-0057 access-depth axis readScope / writeScope — so one click on R / CRUD / All silently deleted whatever those held, and both save doors persisted the truncated row (the environment door writes the whole record; at package scope mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086 P0, so base cannot restore them). The sharpest shape: the button labelled All could WIDEN effective read access by deleting a readScope: own narrowing, with no diff shown and no error.

The granting arms (all / crud / read) now start from the current row, reset the keys the matrix authors (OBJECT_ACTIONS), then set the granted ones — so unmodelled keys ride through exactly as they already do on the per-checkbox path (updateObjectPerm spreads). Resetting the authored keys first keeps today's bulk semantics for the matrix's own columns: CRUD after All still means exactly CRUD, not "add CRUD".

none is deliberately UNCHANGED — it still replaces the row with {}

The card's suggested fix asked for the same merge treatment on none. Per the PM dispatch on #6605 this PR does not do that, deliberately: the defect is a grant that silently drops a narrowing; none grants nothing, so nothing survives for a scope to narrow. Merging none would leave allowExport: true (and the scopes) alive after a click on the button labelled None — a permissive outcome that does not exist today, on a surface whose whole problem is silent permissiveness. What an admin's "None" means is a behaviour decision, and it was made on the card thread, not here. The none arm is pinned (PermissionMatrixEditor.bulkMergeKeys.test.tsx) so a later refactor cannot quietly adopt the card's suggestion without going red and surfacing that decision.

Load-bearing measurement behind that fence, re-taken on the merged ref (post #6607): OBJECT_ACTIONS contains exactly allowCreate, allowRead, allowEdit, allowDelete, allowTransfer, viewAllRecords, modifyAllRecords — no allowExport, no scopes — so all three dropped keys are "shown to reviewers, not authored by the matrix", as the dispatch asserted.

Tests (all assert the SAVED payload, never editor state)

New PermissionMatrixEditor.bulkMergeKeys.test.tsx, 5 pins:

  • All (the widening shape): a row carrying allowExport: true, readScope: own, writeScope: own gets every matrix column granted AND keeps all three keys in the saved payload.
  • CRUD: unmodelled keys ride through, while allowTransfer / viewAllRecords / modifyAllRecords still reset — the falsification direction that merge did not decay into "add".
  • R: saved row is exactly allowRead plus the three unmodelled keys.
  • None: saved row is exactly {} — with a positive control in the same payload (an untouched sibling row still carries its readScope), so the emptiness measures none, not a key-dropping save path.
  • Package door: All under a packageId — the merged slice keeps the three keys, another package's out-of-scope row survives byte-for-byte, and the save went through the draft door (mode: draft).

Every pin was shown going red (fix committed first; restores proven by observation — git diff HEAD empty AND blob-hash match against HEAD, never exit codes; mutations proven on disk by anchored grep counts flipping plus a blob-hash difference; no rebuild needed for either leg — the suite imports the mutated file by relative path from source, no dist resolution in the loop):

  • Leg A (revert editor to pre-fix 2a23000f): predicted the four merge pins red, none pin green — observed exactly that (Tests 4 failed | 1 passed), failing in the predicted direction (expected undefined to be 'own').
  • Leg B (mutate none to the card's suggested merge): predicted only the none pin red — observed exactly that (Tests 1 failed | 4 passed), failing with expected { allowExport: true, … } to deeply equal {} — the precise hazard the dispatch fence names.

Verification at final commit b0517791 (all via the shared verify lock where heavy):

  • pnpm exec vitest run (repo root) over the new file + retiredLifecycleKeys + packageDoorFacets + both permission-slice suites: Test Files 5 passed (5) · Tests 21 passed (21), lock VERDICT command-exit 0.
  • pnpm run type-check in app-shell (both tsconfigs): exit 0, and tsc -p tsconfig.test.json --listFiles shows the new test file in the checked set (nonzero, with a nonzero positive control on a sibling test file).
  • turbo run lint --filter=@object-ui/app-shell: 0 errors (pre-existing package-wide warnings only), lock VERDICT command-exit 0.
  • Derived gates for this diff (objectui has no dispatch-gates script; derived by hand from package.json + workflows): check-control-bytes: OK, check-vi-mock-specifiers: OK (new vi.mock call sites), i18n call-site keys OK, designer-field-key-parity: OK, changeset presence ✅ … declares 1 changeset(s), changeset no-major ✅ No changeset declares a major bump.

Scope

Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49

Generated by Claude Code


Generated by Claude Code

…f replacing it (#6605)
The R / CRUD / All bulk buttons in the permission matrix replaced the
object's row wholesale, silently deleting the spec-declared keys the
matrix does not author: allowExport and the ADR-0057 access-depth axis
readScope / writeScope. Both save doors persisted the truncated row (the
environment door writes the whole record; at package scope
mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086
P0, so base cannot restore them). The sharpest shape: the button
labelled All could WIDEN effective read access by dropping a
readScope: 'own' narrowing with no diff and no error.
The granting arms now start from the current row, reset the keys the
matrix authors (OBJECT_ACTIONS), and set the granted ones — unmodelled
keys ride through exactly as they do on the per-checkbox path
(updateObjectPerm's spread). None deliberately keeps replacing with {}:
it grants nothing, so nothing survives for a scope to narrow, and
merging there would leave allowExport: true alive after a click on the
button labelled None — a permissive outcome that does not exist today.
That fence is pinned by PermissionMatrixEditor.bulkMergeKeys.test.tsx,
which asserts the SAVED payload (both doors), never editor state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-CWRw4V5a.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT on substance (ui execution seat, session_01CRJge11jso9TpXRWFt1Z49), head b0517791. Landing gated on the farm only — 29/29 registered, 0 failures, nine checks still running. ⛔ Not flipped: every check green on the reviewed head, not the required subset.

The fence held, and it held on measurement rather than on my say-so

My dispatch order fenced the none arm out against the card's own suggested fix, and said plainly that the reasoning rested on an unmeasured PM assertion — that OBJECT_ACTIONS excludes allowExport. Measured on the merged ref: OBJECT_ACTIONS is exactly allowCreate / allowRead / allowEdit / allowDelete / allowTransfer / viewAllRecords / modifyAllRecords. No allowExport, no scopes. The assertion was right, and it is now a reading instead of a guess.

⭐⭐ Leg B of the ablation is the part I want kept. It mutated the none arm into the card's suggested merge and observed expected { allowExport: true, … } to deeply equal {} — the exact hazard the fence names, reproduced inside an assertion. That converts "the PM argued none must not merge" into "a pin goes red if anyone makes it merge." ⭐ A fence that lives only in a dispatch comment is a fence until the next refactor; a fence with a pin is a fence.

A hazard the order did not name, caught anyway

A naive merge would have made CRUD-after-All leave allowTransfer / viewAllRecords / modifyAllRecords true — merge silently becoming add. The implementation deletes the OBJECT_ACTIONS keys first and then sets the granted ones, so bulk still resets matrix-owned columns while unmodelled keys ride through. That distinction is the whole correctness of the fix and it is pinned as its own falsification case. I did not ask for it.

Every pin asserts the SAVED payload

Not editor state — which matters here specifically, because the card's own argument for why this persists is that both save doors carry the truncated row and mergePermissionSlice takes in-scope rows entirely from edited. ⭐ A test that stopped at editor state would have proved nothing about the defect. The package-door pin also checks another package's row survives byte-for-byte, and the none pin carries a positive control in the same payload (an untouched sibling row keeps readScope: 'own'), so "saved an empty row" cannot pass by everything being empty.

The root-cause hypothesis was assessed and declined, correctly

I offered deriving ObjectPerm from the spec's ObjectPermission — the shape PR #6618 landed in this same directory — explicitly as a hypothesis, not an instruction. It was measured and turned down with a real reason: keyof ObjectPerm is the editor-wide boolean-checkbox key type, while the spec type carries the non-boolean readScope / writeScope enums plus still-declared retired lifecycle keys, so derivation forces a boolean-key split across editor props — and without deriving OBJECT_ACTIONS too it buys no compile-time guarantee. ⭐ That is the right answer to a PM's speculation: measure it, price it, decline it in writing. ⛔ Widening the PR on my hunch would have been the wrong call.

Scope

Fixes #6605 is correct — all four arms are disposed of inside the fence: three fixed, none deliberately unchanged and pinned. #6606 (the gate-coverage half — why nothing caught this) is correctly untouched and stays with domain:devx.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 27, 2026 19:03
@os-sales
os-sales added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 38268abAug 27, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6605-permission-bulk-merge-not-replace branch August 27, 2026 19:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permission matrix bulk buttons (R / CRUD / All / None) silently delete allowExport, readScope and writeScope from the row

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(app-shell): permission matrix bulk grants merge the row instead of replacing it by os-sales · Pull Request #6623 · objectstack-ai/objectui · GitHub
Skip to content

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it - #6623

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace
Aug 27, 2026
Merged

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it#6623
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6605

What

bulkSetObject in PermissionMatrixEditor.tsx replaced an object's permission row wholesale on every bulk click. Three spec-declared keys are modelled by neither the local ObjectPerm interface nor the OBJECT_ACTIONS column list — allowExport, and the ADR-0057 access-depth axis readScope / writeScope — so one click on R / CRUD / All silently deleted whatever those held, and both save doors persisted the truncated row (the environment door writes the whole record; at package scope mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086 P0, so base cannot restore them). The sharpest shape: the button labelled All could WIDEN effective read access by deleting a readScope: own narrowing, with no diff shown and no error.

The granting arms (all / crud / read) now start from the current row, reset the keys the matrix authors (OBJECT_ACTIONS), then set the granted ones — so unmodelled keys ride through exactly as they already do on the per-checkbox path (updateObjectPerm spreads). Resetting the authored keys first keeps today's bulk semantics for the matrix's own columns: CRUD after All still means exactly CRUD, not "add CRUD".

none is deliberately UNCHANGED — it still replaces the row with {}

The card's suggested fix asked for the same merge treatment on none. Per the PM dispatch on #6605 this PR does not do that, deliberately: the defect is a grant that silently drops a narrowing; none grants nothing, so nothing survives for a scope to narrow. Merging none would leave allowExport: true (and the scopes) alive after a click on the button labelled None — a permissive outcome that does not exist today, on a surface whose whole problem is silent permissiveness. What an admin's "None" means is a behaviour decision, and it was made on the card thread, not here. The none arm is pinned (PermissionMatrixEditor.bulkMergeKeys.test.tsx) so a later refactor cannot quietly adopt the card's suggestion without going red and surfacing that decision.

Load-bearing measurement behind that fence, re-taken on the merged ref (post #6607): OBJECT_ACTIONS contains exactly allowCreate, allowRead, allowEdit, allowDelete, allowTransfer, viewAllRecords, modifyAllRecords — no allowExport, no scopes — so all three dropped keys are "shown to reviewers, not authored by the matrix", as the dispatch asserted.

Tests (all assert the SAVED payload, never editor state)

New PermissionMatrixEditor.bulkMergeKeys.test.tsx, 5 pins:

  • All (the widening shape): a row carrying allowExport: true, readScope: own, writeScope: own gets every matrix column granted AND keeps all three keys in the saved payload.
  • CRUD: unmodelled keys ride through, while allowTransfer / viewAllRecords / modifyAllRecords still reset — the falsification direction that merge did not decay into "add".
  • R: saved row is exactly allowRead plus the three unmodelled keys.
  • None: saved row is exactly {} — with a positive control in the same payload (an untouched sibling row still carries its readScope), so the emptiness measures none, not a key-dropping save path.
  • Package door: All under a packageId — the merged slice keeps the three keys, another package's out-of-scope row survives byte-for-byte, and the save went through the draft door (mode: draft).

Every pin was shown going red (fix committed first; restores proven by observation — git diff HEAD empty AND blob-hash match against HEAD, never exit codes; mutations proven on disk by anchored grep counts flipping plus a blob-hash difference; no rebuild needed for either leg — the suite imports the mutated file by relative path from source, no dist resolution in the loop):

  • Leg A (revert editor to pre-fix 2a23000f): predicted the four merge pins red, none pin green — observed exactly that (Tests 4 failed | 1 passed), failing in the predicted direction (expected undefined to be 'own').
  • Leg B (mutate none to the card's suggested merge): predicted only the none pin red — observed exactly that (Tests 1 failed | 4 passed), failing with expected { allowExport: true, … } to deeply equal {} — the precise hazard the dispatch fence names.

Verification at final commit b0517791 (all via the shared verify lock where heavy):

  • pnpm exec vitest run (repo root) over the new file + retiredLifecycleKeys + packageDoorFacets + both permission-slice suites: Test Files 5 passed (5) · Tests 21 passed (21), lock VERDICT command-exit 0.
  • pnpm run type-check in app-shell (both tsconfigs): exit 0, and tsc -p tsconfig.test.json --listFiles shows the new test file in the checked set (nonzero, with a nonzero positive control on a sibling test file).
  • turbo run lint --filter=@object-ui/app-shell: 0 errors (pre-existing package-wide warnings only), lock VERDICT command-exit 0.
  • Derived gates for this diff (objectui has no dispatch-gates script; derived by hand from package.json + workflows): check-control-bytes: OK, check-vi-mock-specifiers: OK (new vi.mock call sites), i18n call-site keys OK, designer-field-key-parity: OK, changeset presence ✅ … declares 1 changeset(s), changeset no-major ✅ No changeset declares a major bump.

Scope

Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49

Generated by Claude Code


Generated by Claude Code

…f replacing it (#6605)
The R / CRUD / All bulk buttons in the permission matrix replaced the
object's row wholesale, silently deleting the spec-declared keys the
matrix does not author: allowExport and the ADR-0057 access-depth axis
readScope / writeScope. Both save doors persisted the truncated row (the
environment door writes the whole record; at package scope
mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086
P0, so base cannot restore them). The sharpest shape: the button
labelled All could WIDEN effective read access by dropping a
readScope: 'own' narrowing with no diff and no error.
The granting arms now start from the current row, reset the keys the
matrix authors (OBJECT_ACTIONS), and set the granted ones — unmodelled
keys ride through exactly as they do on the per-checkbox path
(updateObjectPerm's spread). None deliberately keeps replacing with {}:
it grants nothing, so nothing survives for a scope to narrow, and
merging there would leave allowExport: true alive after a click on the
button labelled None — a permissive outcome that does not exist today.
That fence is pinned by PermissionMatrixEditor.bulkMergeKeys.test.tsx,
which asserts the SAVED payload (both doors), never editor state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-CWRw4V5a.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT on substance (ui execution seat, session_01CRJge11jso9TpXRWFt1Z49), head b0517791. Landing gated on the farm only — 29/29 registered, 0 failures, nine checks still running. ⛔ Not flipped: every check green on the reviewed head, not the required subset.

The fence held, and it held on measurement rather than on my say-so

My dispatch order fenced the none arm out against the card's own suggested fix, and said plainly that the reasoning rested on an unmeasured PM assertion — that OBJECT_ACTIONS excludes allowExport. Measured on the merged ref: OBJECT_ACTIONS is exactly allowCreate / allowRead / allowEdit / allowDelete / allowTransfer / viewAllRecords / modifyAllRecords. No allowExport, no scopes. The assertion was right, and it is now a reading instead of a guess.

⭐⭐ Leg B of the ablation is the part I want kept. It mutated the none arm into the card's suggested merge and observed expected { allowExport: true, … } to deeply equal {} — the exact hazard the fence names, reproduced inside an assertion. That converts "the PM argued none must not merge" into "a pin goes red if anyone makes it merge." ⭐ A fence that lives only in a dispatch comment is a fence until the next refactor; a fence with a pin is a fence.

A hazard the order did not name, caught anyway

A naive merge would have made CRUD-after-All leave allowTransfer / viewAllRecords / modifyAllRecords true — merge silently becoming add. The implementation deletes the OBJECT_ACTIONS keys first and then sets the granted ones, so bulk still resets matrix-owned columns while unmodelled keys ride through. That distinction is the whole correctness of the fix and it is pinned as its own falsification case. I did not ask for it.

Every pin asserts the SAVED payload

Not editor state — which matters here specifically, because the card's own argument for why this persists is that both save doors carry the truncated row and mergePermissionSlice takes in-scope rows entirely from edited. ⭐ A test that stopped at editor state would have proved nothing about the defect. The package-door pin also checks another package's row survives byte-for-byte, and the none pin carries a positive control in the same payload (an untouched sibling row keeps readScope: 'own'), so "saved an empty row" cannot pass by everything being empty.

The root-cause hypothesis was assessed and declined, correctly

I offered deriving ObjectPerm from the spec's ObjectPermission — the shape PR #6618 landed in this same directory — explicitly as a hypothesis, not an instruction. It was measured and turned down with a real reason: keyof ObjectPerm is the editor-wide boolean-checkbox key type, while the spec type carries the non-boolean readScope / writeScope enums plus still-declared retired lifecycle keys, so derivation forces a boolean-key split across editor props — and without deriving OBJECT_ACTIONS too it buys no compile-time guarantee. ⭐ That is the right answer to a PM's speculation: measure it, price it, decline it in writing. ⛔ Widening the PR on my hunch would have been the wrong call.

Scope

Fixes #6605 is correct — all four arms are disposed of inside the fence: three fixed, none deliberately unchanged and pinned. #6606 (the gate-coverage half — why nothing caught this) is correctly untouched and stays with domain:devx.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 27, 2026 19:03
@os-sales
os-sales added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 38268abAug 27, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6605-permission-bulk-merge-not-replace branch August 27, 2026 19:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permission matrix bulk buttons (R / CRUD / All / None) silently delete allowExport, readScope and writeScope from the row

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(app-shell): permission matrix bulk grants merge the row instead of replacing it by os-sales · Pull Request #6623 · objectstack-ai/objectui · GitHub
Skip to content

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it - #6623

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace
Aug 27, 2026
Merged

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it#6623
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6605

What

bulkSetObject in PermissionMatrixEditor.tsx replaced an object's permission row wholesale on every bulk click. Three spec-declared keys are modelled by neither the local ObjectPerm interface nor the OBJECT_ACTIONS column list — allowExport, and the ADR-0057 access-depth axis readScope / writeScope — so one click on R / CRUD / All silently deleted whatever those held, and both save doors persisted the truncated row (the environment door writes the whole record; at package scope mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086 P0, so base cannot restore them). The sharpest shape: the button labelled All could WIDEN effective read access by deleting a readScope: own narrowing, with no diff shown and no error.

The granting arms (all / crud / read) now start from the current row, reset the keys the matrix authors (OBJECT_ACTIONS), then set the granted ones — so unmodelled keys ride through exactly as they already do on the per-checkbox path (updateObjectPerm spreads). Resetting the authored keys first keeps today's bulk semantics for the matrix's own columns: CRUD after All still means exactly CRUD, not "add CRUD".

none is deliberately UNCHANGED — it still replaces the row with {}

The card's suggested fix asked for the same merge treatment on none. Per the PM dispatch on #6605 this PR does not do that, deliberately: the defect is a grant that silently drops a narrowing; none grants nothing, so nothing survives for a scope to narrow. Merging none would leave allowExport: true (and the scopes) alive after a click on the button labelled None — a permissive outcome that does not exist today, on a surface whose whole problem is silent permissiveness. What an admin's "None" means is a behaviour decision, and it was made on the card thread, not here. The none arm is pinned (PermissionMatrixEditor.bulkMergeKeys.test.tsx) so a later refactor cannot quietly adopt the card's suggestion without going red and surfacing that decision.

Load-bearing measurement behind that fence, re-taken on the merged ref (post #6607): OBJECT_ACTIONS contains exactly allowCreate, allowRead, allowEdit, allowDelete, allowTransfer, viewAllRecords, modifyAllRecords — no allowExport, no scopes — so all three dropped keys are "shown to reviewers, not authored by the matrix", as the dispatch asserted.

Tests (all assert the SAVED payload, never editor state)

New PermissionMatrixEditor.bulkMergeKeys.test.tsx, 5 pins:

  • All (the widening shape): a row carrying allowExport: true, readScope: own, writeScope: own gets every matrix column granted AND keeps all three keys in the saved payload.
  • CRUD: unmodelled keys ride through, while allowTransfer / viewAllRecords / modifyAllRecords still reset — the falsification direction that merge did not decay into "add".
  • R: saved row is exactly allowRead plus the three unmodelled keys.
  • None: saved row is exactly {} — with a positive control in the same payload (an untouched sibling row still carries its readScope), so the emptiness measures none, not a key-dropping save path.
  • Package door: All under a packageId — the merged slice keeps the three keys, another package's out-of-scope row survives byte-for-byte, and the save went through the draft door (mode: draft).

Every pin was shown going red (fix committed first; restores proven by observation — git diff HEAD empty AND blob-hash match against HEAD, never exit codes; mutations proven on disk by anchored grep counts flipping plus a blob-hash difference; no rebuild needed for either leg — the suite imports the mutated file by relative path from source, no dist resolution in the loop):

  • Leg A (revert editor to pre-fix 2a23000f): predicted the four merge pins red, none pin green — observed exactly that (Tests 4 failed | 1 passed), failing in the predicted direction (expected undefined to be 'own').
  • Leg B (mutate none to the card's suggested merge): predicted only the none pin red — observed exactly that (Tests 1 failed | 4 passed), failing with expected { allowExport: true, … } to deeply equal {} — the precise hazard the dispatch fence names.

Verification at final commit b0517791 (all via the shared verify lock where heavy):

  • pnpm exec vitest run (repo root) over the new file + retiredLifecycleKeys + packageDoorFacets + both permission-slice suites: Test Files 5 passed (5) · Tests 21 passed (21), lock VERDICT command-exit 0.
  • pnpm run type-check in app-shell (both tsconfigs): exit 0, and tsc -p tsconfig.test.json --listFiles shows the new test file in the checked set (nonzero, with a nonzero positive control on a sibling test file).
  • turbo run lint --filter=@object-ui/app-shell: 0 errors (pre-existing package-wide warnings only), lock VERDICT command-exit 0.
  • Derived gates for this diff (objectui has no dispatch-gates script; derived by hand from package.json + workflows): check-control-bytes: OK, check-vi-mock-specifiers: OK (new vi.mock call sites), i18n call-site keys OK, designer-field-key-parity: OK, changeset presence ✅ … declares 1 changeset(s), changeset no-major ✅ No changeset declares a major bump.

Scope

Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49

Generated by Claude Code


Generated by Claude Code

…f replacing it (#6605)
The R / CRUD / All bulk buttons in the permission matrix replaced the
object's row wholesale, silently deleting the spec-declared keys the
matrix does not author: allowExport and the ADR-0057 access-depth axis
readScope / writeScope. Both save doors persisted the truncated row (the
environment door writes the whole record; at package scope
mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086
P0, so base cannot restore them). The sharpest shape: the button
labelled All could WIDEN effective read access by dropping a
readScope: 'own' narrowing with no diff and no error.
The granting arms now start from the current row, reset the keys the
matrix authors (OBJECT_ACTIONS), and set the granted ones — unmodelled
keys ride through exactly as they do on the per-checkbox path
(updateObjectPerm's spread). None deliberately keeps replacing with {}:
it grants nothing, so nothing survives for a scope to narrow, and
merging there would leave allowExport: true alive after a click on the
button labelled None — a permissive outcome that does not exist today.
That fence is pinned by PermissionMatrixEditor.bulkMergeKeys.test.tsx,
which asserts the SAVED payload (both doors), never editor state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-CWRw4V5a.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT on substance (ui execution seat, session_01CRJge11jso9TpXRWFt1Z49), head b0517791. Landing gated on the farm only — 29/29 registered, 0 failures, nine checks still running. ⛔ Not flipped: every check green on the reviewed head, not the required subset.

The fence held, and it held on measurement rather than on my say-so

My dispatch order fenced the none arm out against the card's own suggested fix, and said plainly that the reasoning rested on an unmeasured PM assertion — that OBJECT_ACTIONS excludes allowExport. Measured on the merged ref: OBJECT_ACTIONS is exactly allowCreate / allowRead / allowEdit / allowDelete / allowTransfer / viewAllRecords / modifyAllRecords. No allowExport, no scopes. The assertion was right, and it is now a reading instead of a guess.

⭐⭐ Leg B of the ablation is the part I want kept. It mutated the none arm into the card's suggested merge and observed expected { allowExport: true, … } to deeply equal {} — the exact hazard the fence names, reproduced inside an assertion. That converts "the PM argued none must not merge" into "a pin goes red if anyone makes it merge." ⭐ A fence that lives only in a dispatch comment is a fence until the next refactor; a fence with a pin is a fence.

A hazard the order did not name, caught anyway

A naive merge would have made CRUD-after-All leave allowTransfer / viewAllRecords / modifyAllRecords true — merge silently becoming add. The implementation deletes the OBJECT_ACTIONS keys first and then sets the granted ones, so bulk still resets matrix-owned columns while unmodelled keys ride through. That distinction is the whole correctness of the fix and it is pinned as its own falsification case. I did not ask for it.

Every pin asserts the SAVED payload

Not editor state — which matters here specifically, because the card's own argument for why this persists is that both save doors carry the truncated row and mergePermissionSlice takes in-scope rows entirely from edited. ⭐ A test that stopped at editor state would have proved nothing about the defect. The package-door pin also checks another package's row survives byte-for-byte, and the none pin carries a positive control in the same payload (an untouched sibling row keeps readScope: 'own'), so "saved an empty row" cannot pass by everything being empty.

The root-cause hypothesis was assessed and declined, correctly

I offered deriving ObjectPerm from the spec's ObjectPermission — the shape PR #6618 landed in this same directory — explicitly as a hypothesis, not an instruction. It was measured and turned down with a real reason: keyof ObjectPerm is the editor-wide boolean-checkbox key type, while the spec type carries the non-boolean readScope / writeScope enums plus still-declared retired lifecycle keys, so derivation forces a boolean-key split across editor props — and without deriving OBJECT_ACTIONS too it buys no compile-time guarantee. ⭐ That is the right answer to a PM's speculation: measure it, price it, decline it in writing. ⛔ Widening the PR on my hunch would have been the wrong call.

Scope

Fixes #6605 is correct — all four arms are disposed of inside the fence: three fixed, none deliberately unchanged and pinned. #6606 (the gate-coverage half — why nothing caught this) is correctly untouched and stays with domain:devx.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 27, 2026 19:03
@os-sales
os-sales added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 38268abAug 27, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6605-permission-bulk-merge-not-replace branch August 27, 2026 19:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permission matrix bulk buttons (R / CRUD / All / None) silently delete allowExport, readScope and writeScope from the row

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(app-shell): permission matrix bulk grants merge the row instead of replacing it by os-sales · Pull Request #6623 · objectstack-ai/objectui · GitHub
Skip to content

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it - #6623

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace
Aug 27, 2026
Merged

fix(app-shell): permission matrix bulk grants merge the row instead of replacing it#6623
os-sales merged 1 commit into
mainfrom
claude/issue-6605-permission-bulk-merge-not-replace

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6605

What

bulkSetObject in PermissionMatrixEditor.tsx replaced an object's permission row wholesale on every bulk click. Three spec-declared keys are modelled by neither the local ObjectPerm interface nor the OBJECT_ACTIONS column list — allowExport, and the ADR-0057 access-depth axis readScope / writeScope — so one click on R / CRUD / All silently deleted whatever those held, and both save doors persisted the truncated row (the environment door writes the whole record; at package scope mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086 P0, so base cannot restore them). The sharpest shape: the button labelled All could WIDEN effective read access by deleting a readScope: own narrowing, with no diff shown and no error.

The granting arms (all / crud / read) now start from the current row, reset the keys the matrix authors (OBJECT_ACTIONS), then set the granted ones — so unmodelled keys ride through exactly as they already do on the per-checkbox path (updateObjectPerm spreads). Resetting the authored keys first keeps today's bulk semantics for the matrix's own columns: CRUD after All still means exactly CRUD, not "add CRUD".

none is deliberately UNCHANGED — it still replaces the row with {}

The card's suggested fix asked for the same merge treatment on none. Per the PM dispatch on #6605 this PR does not do that, deliberately: the defect is a grant that silently drops a narrowing; none grants nothing, so nothing survives for a scope to narrow. Merging none would leave allowExport: true (and the scopes) alive after a click on the button labelled None — a permissive outcome that does not exist today, on a surface whose whole problem is silent permissiveness. What an admin's "None" means is a behaviour decision, and it was made on the card thread, not here. The none arm is pinned (PermissionMatrixEditor.bulkMergeKeys.test.tsx) so a later refactor cannot quietly adopt the card's suggestion without going red and surfacing that decision.

Load-bearing measurement behind that fence, re-taken on the merged ref (post #6607): OBJECT_ACTIONS contains exactly allowCreate, allowRead, allowEdit, allowDelete, allowTransfer, viewAllRecords, modifyAllRecords — no allowExport, no scopes — so all three dropped keys are "shown to reviewers, not authored by the matrix", as the dispatch asserted.

Tests (all assert the SAVED payload, never editor state)

New PermissionMatrixEditor.bulkMergeKeys.test.tsx, 5 pins:

  • All (the widening shape): a row carrying allowExport: true, readScope: own, writeScope: own gets every matrix column granted AND keeps all three keys in the saved payload.
  • CRUD: unmodelled keys ride through, while allowTransfer / viewAllRecords / modifyAllRecords still reset — the falsification direction that merge did not decay into "add".
  • R: saved row is exactly allowRead plus the three unmodelled keys.
  • None: saved row is exactly {} — with a positive control in the same payload (an untouched sibling row still carries its readScope), so the emptiness measures none, not a key-dropping save path.
  • Package door: All under a packageId — the merged slice keeps the three keys, another package's out-of-scope row survives byte-for-byte, and the save went through the draft door (mode: draft).

Every pin was shown going red (fix committed first; restores proven by observation — git diff HEAD empty AND blob-hash match against HEAD, never exit codes; mutations proven on disk by anchored grep counts flipping plus a blob-hash difference; no rebuild needed for either leg — the suite imports the mutated file by relative path from source, no dist resolution in the loop):

  • Leg A (revert editor to pre-fix 2a23000f): predicted the four merge pins red, none pin green — observed exactly that (Tests 4 failed | 1 passed), failing in the predicted direction (expected undefined to be 'own').
  • Leg B (mutate none to the card's suggested merge): predicted only the none pin red — observed exactly that (Tests 1 failed | 4 passed), failing with expected { allowExport: true, … } to deeply equal {} — the precise hazard the dispatch fence names.

Verification at final commit b0517791 (all via the shared verify lock where heavy):

  • pnpm exec vitest run (repo root) over the new file + retiredLifecycleKeys + packageDoorFacets + both permission-slice suites: Test Files 5 passed (5) · Tests 21 passed (21), lock VERDICT command-exit 0.
  • pnpm run type-check in app-shell (both tsconfigs): exit 0, and tsc -p tsconfig.test.json --listFiles shows the new test file in the checked set (nonzero, with a nonzero positive control on a sibling test file).
  • turbo run lint --filter=@object-ui/app-shell: 0 errors (pre-existing package-wide warnings only), lock VERDICT command-exit 0.
  • Derived gates for this diff (objectui has no dispatch-gates script; derived by hand from package.json + workflows): check-control-bytes: OK, check-vi-mock-specifiers: OK (new vi.mock call sites), i18n call-site keys OK, designer-field-key-parity: OK, changeset presence ✅ … declares 1 changeset(s), changeset no-major ✅ No changeset declares a major bump.

Scope

Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49

Generated by Claude Code


Generated by Claude Code

…f replacing it (#6605)
The R / CRUD / All bulk buttons in the permission matrix replaced the
object's row wholesale, silently deleting the spec-declared keys the
matrix does not author: allowExport and the ADR-0057 access-depth axis
readScope / writeScope. Both save doors persisted the truncated row (the
environment door writes the whole record; at package scope
mergePermissionSlice takes in-scope rows entirely from edited, ADR-0086
P0, so base cannot restore them). The sharpest shape: the button
labelled All could WIDEN effective read access by dropping a
readScope: 'own' narrowing with no diff and no error.
The granting arms now start from the current row, reset the keys the
matrix authors (OBJECT_ACTIONS), and set the granted ones — unmodelled
keys ride through exactly as they do on the per-checkbox path
(updateObjectPerm's spread). None deliberately keeps replacing with {}:
it grants nothing, so nothing survives for a scope to narrow, and
merging there would leave allowExport: true alive after a click on the
button labelled None — a permissive outcome that does not exist today.
That fence is pinned by PermissionMatrixEditor.bulkMergeKeys.test.tsx,
which asserts the SAVED payload (both doors), never editor state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-CWRw4V5a.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT on substance (ui execution seat, session_01CRJge11jso9TpXRWFt1Z49), head b0517791. Landing gated on the farm only — 29/29 registered, 0 failures, nine checks still running. ⛔ Not flipped: every check green on the reviewed head, not the required subset.

The fence held, and it held on measurement rather than on my say-so

My dispatch order fenced the none arm out against the card's own suggested fix, and said plainly that the reasoning rested on an unmeasured PM assertion — that OBJECT_ACTIONS excludes allowExport. Measured on the merged ref: OBJECT_ACTIONS is exactly allowCreate / allowRead / allowEdit / allowDelete / allowTransfer / viewAllRecords / modifyAllRecords. No allowExport, no scopes. The assertion was right, and it is now a reading instead of a guess.

⭐⭐ Leg B of the ablation is the part I want kept. It mutated the none arm into the card's suggested merge and observed expected { allowExport: true, … } to deeply equal {} — the exact hazard the fence names, reproduced inside an assertion. That converts "the PM argued none must not merge" into "a pin goes red if anyone makes it merge." ⭐ A fence that lives only in a dispatch comment is a fence until the next refactor; a fence with a pin is a fence.

A hazard the order did not name, caught anyway

A naive merge would have made CRUD-after-All leave allowTransfer / viewAllRecords / modifyAllRecords true — merge silently becoming add. The implementation deletes the OBJECT_ACTIONS keys first and then sets the granted ones, so bulk still resets matrix-owned columns while unmodelled keys ride through. That distinction is the whole correctness of the fix and it is pinned as its own falsification case. I did not ask for it.

Every pin asserts the SAVED payload

Not editor state — which matters here specifically, because the card's own argument for why this persists is that both save doors carry the truncated row and mergePermissionSlice takes in-scope rows entirely from edited. ⭐ A test that stopped at editor state would have proved nothing about the defect. The package-door pin also checks another package's row survives byte-for-byte, and the none pin carries a positive control in the same payload (an untouched sibling row keeps readScope: 'own'), so "saved an empty row" cannot pass by everything being empty.

The root-cause hypothesis was assessed and declined, correctly

I offered deriving ObjectPerm from the spec's ObjectPermission — the shape PR #6618 landed in this same directory — explicitly as a hypothesis, not an instruction. It was measured and turned down with a real reason: keyof ObjectPerm is the editor-wide boolean-checkbox key type, while the spec type carries the non-boolean readScope / writeScope enums plus still-declared retired lifecycle keys, so derivation forces a boolean-key split across editor props — and without deriving OBJECT_ACTIONS too it buys no compile-time guarantee. ⭐ That is the right answer to a PM's speculation: measure it, price it, decline it in writing. ⛔ Widening the PR on my hunch would have been the wrong call.

Scope

Fixes #6605 is correct — all four arms are disposed of inside the fence: three fixed, none deliberately unchanged and pinned. #6606 (the gate-coverage half — why nothing caught this) is correctly untouched and stays with domain:devx.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 27, 2026 19:03
@os-sales
os-sales added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 38268abAug 27, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6605-permission-bulk-merge-not-replace branch August 27, 2026 19:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permission matrix bulk buttons (R / CRUD / All / None) silently delete allowExport, readScope and writeScope from the row

2 participants

@os-sales@claude