Skip to content

refactor(app-shell): call owd-sharing's isExternalWider from ObjectSettingsPanel, pinned across both surfaces - #5579

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-5477-owd-width-single-home
Aug 21, 2026
Merged

refactor(app-shell): call owd-sharing's isExternalWider from ObjectSettingsPanel, pinned across both surfaces#5579
os-sales merged 1 commit into
mainfrom
claude/issue-5477-owd-width-single-home

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#5477

ObjectSettingsPanel re-declared OWD_WIDTH and the ADR-0090 D11 "external must never be wider than internal" comparison inline, while owd-sharing.ts — which states its own purpose as "the single home" for the pieces this tab and PackageOwdOverviewPanel must agree on — already exported isExternalWider doing the same thing. This swaps the inline block for the import and adds the agreement pin the finding asked for.

The two copies AGREED — this is not a behaviour change

The card warned they might have drifted. They had not, and that was measured rather than read off. Both implementations were transcribed verbatim from origin/main @ ac73c24b0 and enumerated over the full input domain — the five values either dial offers, the rejected legacy aliases (read / read_write / full), undefined, and prototype-chain keys (toString, constructor, __proto__) that in reaches:

pairs=144 inline_true=6 disagreements=0

The module's extra !!internal / !!external truthiness guards are redundant at this call site, which already normalizes both dials to '' via typeof … === 'string' ? … : '', and '' in OWD_WIDTH is false regardless. Over the 25 pairs the dials can actually produce, the same three warn before and after:

private → public_read, private → public_read_write, public_read → public_read_write

Nothing an author sees changes. The one thing worth a reviewer's eye is argument order: the module takes (internal, external), the reverse of how the violation reads, so the call is isExternalWider(sharingModel, externalSharingModel). A comment at the call site says so, and the pin below fails if it is ever swapped.

PackageOwdOverviewPanel was already a correct consumer

Checked before choosing the shape, per the card's request: it is not a second inline copy. PackageOwdOverviewPanel.tsx:34 imports isExternalWider from ./owd-sharing.js and calls it at line 204. So the Settings tab was the only surface not calling the shared module, and no separate finding is needed.

The pin (the substantive half)

ObjectSettingsPanel.owdAgreement.test.tsx deliberately does not assert that the panel calls a function — that proves nothing about whether the comparison is the same one, since a re-inlined copy calls nothing and a call spy stays green when the arguments are swapped. Instead it asserts behaviour across the boundary the drift would cross, the two surfaces: both are driven over the full 5×5 cross-product of the values their dials offer, each verdict is read off what the surface renders (the Settings tab's amber D11 hint; the overview's per-row owd-error-* node), and all three legs — surface A, surface B, and isExternalWider itself — must agree on every pair. The violating pairs are additionally pinned by name so the sweep cannot pass vacuously if every leg silently went false.

Both halves were verified to actually fail, each mutation confirmed on disk by anchored grep before the run and restored by an EXIT INT TERM trap after (tree byte-identical afterwards, git status --porcelain empty both times). No rebuild was needed or claimed: the suite imports these modules by relative source path within the package, not across a dist/exports boundary.

AblationPredictedObserved
Re-inline a drifted copy (>=) into ObjectSettingsPanelagreement leg redred — named all 3 reflexive pairs with ObjectSettingsPanel=true PackageOwdOverviewPanel=false isExternalWider=false
Change D11 in the shared module only (>>=)agreement leg stays green (all three legs now delegate to one implementation); non-vacuity anchor firesexactly that — anchor red on 3 extra pairs

The second is the one worth noting: after convergence there is only one implementation, so a change to it moves all three legs together. That is why the pin carries the named-violations anchor as well as the agreement assertion — the two catch different failure modes.

Gates

All run at 33c983a1a (the final commit; the tree was clean and unchanged from it, both ablations having restored byte-identically). Each verdict is the gate's own printed line, with exit codes captured before any pipe.

  • vitest run packages/app-shell/src/views/studio-design/Test Files 32 passed (32), Tests 187 passed (187)
  • pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json) — TYPECHECK_EXIT=0, no diagnostics. Run afterpnpm --filter '@object-ui/app-shell^...' build; before the closure existed it reported TS2307 on every workspace sibling, which is the stale-closure trap and not a result.
  • pnpm check:control-bytes✅ check-control-bytes: OK (scanned 4630 tracked text file(s); skipped 85 binary)
  • check-changeset-presence✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
  • check-changeset-no-major✅ No changeset declares a 'major' bump.
  • check-changeset-fixed✅ All workspace packages are in the changeset fixed group.

Declared narrowing on pnpm lint. The repo-wide farm is CI's run. Locally eslint was run over the whole affected package, packages/app-shell: 909 files (population from eslint's own config resolution, count from --format json), 0 errors, and 0 errors / 0 warnings on both changed files (the 2508 package warnings are the pre-existing no-unused-vars / no-explicit-any baseline, none on files this PR touches). The narrowing excludes nothing: the config uses tseslint.configs.recommended with no project / projectService, so linting is per-file and syntax-only and each file's verdict is a pure function of its own bytes plus the shared config. This diff changes two .tsx files, both inside app-shell, plus a .md changeset that the **/*.{ts,tsx} files glob does not select — so no file outside app-shell can change verdict.

Scope

Exactly the convergence, per triage's "security-adjacent file, keep the diff to exactly this convergence": three files — ObjectSettingsPanel.tsx, its new test, and the changeset. owd-sharing.ts is untouched (read-only reuse target; it needed no change). No drive-by tidying.


Generated by Claude Code

…ttingsPanel (objectui#5477)
The per-object Settings tab re-declared `OWD_WIDTH` and the ADR-0090 D11
"external must never be wider than internal" comparison inline, while
`owd-sharing.ts` — which states its own purpose as "the single home" for the
pieces this tab and `PackageOwdOverviewPanel` must agree on — exported
`isExternalWider` doing the same thing.
The two were verified equivalent before the swap, over the full domain of both
dials plus `undefined`, the rejected legacy aliases and prototype-chain keys:
144 pairs, zero disagreements. No author-visible verdict changes.
Adds the agreement pin the finding asked for: both surfaces are driven over the
full 5x5 cross-product of the values their dials offer, and the Settings tab's
rendered warning, the overview's per-row error and `isExternalWider` itself must
agree on every pair, with the violating pairs pinned by name so the sweep cannot
pass vacuously.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3784.8 KB3867.2 KB
Main entry chunk (gzip)151.2 KB350 KB
Entry fileindex-DbQNNSC1.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 (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
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)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
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.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
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)506.94KB113.63KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)237.61KB59.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
i18n (useSafeTranslation.js)7.77KB3.13KB
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.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
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.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.51KB32.96KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.48KB20.67KB
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)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
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)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.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-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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-sales
os-sales marked this pull request as ready for review August 21, 2026 14:51
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 3211397Aug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-5477-owd-width-single-home branch August 21, 2026 14:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-sales@claude