Skip to content

Studio's 新建对象 asks for the record-sharing baseline, and an unauthored one is reported before Publish instead of by it - #5480

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-5418-studio-new-object-sharingmodel
Aug 21, 2026
Merged

Studio's 新建对象 asks for the record-sharing baseline, and an unauthored one is reported before Publish instead of by it#5480
os-support-ai merged 4 commits into
mainfrom
claude/issue-5418-studio-new-object-sharingmodel

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5418

Studio's 新建对象 asked for exactly two things and produced an object the publish
door refuses. The gate is correct and is unchanged — an org-wide default has to
be an authored decision. What changes is when the console asks, and when it answers.

The walk, re-verified on current main (9bd7536)

The card was measured on 17.1.0. Every step still reproduces:

StepStatus on mainEvidence
新建对象 asks for exactly two fieldsreproducesCreateItemDialog.tsx renders two Inputs plus an unused extra slot; the object call site passed no extra
the saved draft declares no OWDreproducesbuildObjectSkeleton returned { name, label, fields } — zero sharingModel, counter-probed against 11 hits in ObjectSettingsPanel.tsx
publish refuses itreproducessecurity-owd-unset still errors for any non-system object with owd == null in the framework's packages/lint/src/validate-security-posture.ts
the refusal is a transient toastreproducesdoPublishtoast.error(formatPublishFailures(failed))
nothing pre-publish hints at itreproduceszero OWD preflight in app-shell — the 8 near-hits are the flow simulator, an approval preflight and an i18n column label; counter-probed against 147 validate hits in the same tree

Two things the card did not measure, both of which pin the fix:

  • The framework already does this on its own create path.@objectstack/spec's
    kernel/metadata-create-seeds.ts seeds sharingModel: 'private' for object,
    reasoning that the runtime already resolves an absent value to private
    (fail-closed, ADR-0090 D1) so making it explicit changes no tenant's effective
    sharing. Studio's inline skeleton — which skeletons.ts documents as bypassing the
    registry — is the divergence, not the baseline.
  • The Settings tab actively said the opposite of the truth. Its copy read
    "Not set — the platform defaults to Private (ADR-0090)… Pick an explicit model to
    widen visibility", and its header comment said "leaving it unset is safe". That
    answers what the runtime does and not whether the object can ship.

Surface — located by measurement, as instructed

The dispatch warned not to trust views/metadata-admin/**. Confirmed: that directory
holds the engine.studio.*i18n table and nothing else relevant. The create and
publish paths live in the sibling views/studio-design/, and the review sheet behind
发布 → 全部发布 is preview/DraftChangesPanel.tsx. No package-creation form surface
was touched, so #5416 stays held out.

The shape chosen, and the ones rejected

The card offered three fixes and said any one closes it. This lands 1 and 2, plus
the wording half of 3
.

1 — ask in the create dialog. A third control collects the baseline, pre-selected
to private, glossed with the Settings tab's own strings so two surfaces cannot
describe one security baseline two ways. buildObjectSkeleton takes the value as a
required parameter: a future create path cannot omit the baseline without failing
to type-check.

controlled_by_parent is deliberately not offered at creation — it derives access
from a master relation a brand-new object does not have, so offering it would trade
the security-owd-unset wall for the security-controlled-by-parent-no-relation one.
The framework's own hint draws the same line: "If the object has no master, its
baseline is its own decision — use sharingModel: 'private' (owner + shares),
'public_read', or 'public_read_write'."
The Settings tab keeps all four, where the
object may since have gained the master-detail field.

This is asking, not defaulting around the gate: a visible, labeled, glossed control
pre-set to the value the rule's own hint calls "recommended default". An author who
reads it and accepts it has authored the baseline.

2 — fail early. The pending-changes sheet now runs the framework's own
validateSecurityPosture over the pending object drafts and names any blocking
finding, with its fix-it hint, beside the Publish button. It mirrors the producer's
rule rather than re-deriving it
— a console-local "is sharingModel missing" check
would be a fork of a security gate, free to drift from the door it predicts. It
reports without blocking: the installed lint can legitimately differ from the
version the server enforces, and a console that refuses a publish the server would
accept is the worse failure, because it has no override.

3 — navigable message: partially. The block names the item the way the server's own
refusal does (object/crmext_visit) and says where the control is. It is not a live
link
, because useSurfaceDeepLink captures its target only at mount, so a
?surface= write from an already-mounted pillar moves nothing. Filed as #5476 with the
three candidate routes rather than bolted on here.

Rejected: shortening the duplicated ADR prose in the toast. Tempting — the card
names it — but the duplication is producer-side: the failure's error string already
contains the text issues[] carries. De-duplicating in the consumer is the alias
tolerance Prime Directive #12 forbids. Filed against the producer as
objectstack-ai/objectstack#10524.

Rejected: seeding sharingModel in the skeleton with no UI. It would make the
error go away and leave the decision unauthored — the manual floor, not this card's.

Clause ② — yes

This changes what the console produces: a 新建对象 draft now carries
sharingModel. It does not change what the platform accepts, and no gate was
weakened.

Reverse-verification

Both legs are source-level within packages/app-shell — the tests import the ablated
modules by relative path, not across a package exports boundary — so no dist
rebuild is involved
; the only cross-package resolution in play is the unmodified
published @objectstack/lint. Direction was predicted before each run.

LegPredictedObserved
buildObjectSkeleton drops sharingModel2 failed / 3 passed — only the two body-asserting casesexactly that, asserting on { name: 'visit', label: 'Visit', fields } — the card's original defect shape
the sheet stops rendering the problem block2 failed / 1 passed, the survivor a vacuous pass (it asserts absence)exactly that — which is why the other two exist

Both restored; git status clean at each step.

Verification

Run on the final commit f89e0a053:

  • pnpm exec vitest run packages/app-shell/src/preview/ packages/app-shell/src/views/studio-design/ packages/app-shell/src/views/metadata-admin/ packages/i18n --maxWorkers=2
    273 files, 3056 passed, 1 skipped. The skip is a pre-existing it.skipIf
    version-conditional in flow-node-config.spec-reconciliation.test.ts, a file this PR
    does not touch. No test skipped, disabled or quarantined here.
  • type-check on @object-ui/app-shell + @object-ui/i18n → both scripts echoed, exit 0.
    It caught a real widening: OWD_DEFAULT was typed over all four models while the
    create set has three.
  • lint → 0 errors. StudioDesignSurface.tsx is back at its origin/main baseline of
    16 warnings: the first draft's reset-effect added one
    (react-hooks/set-state-in-effect), so the reset moved to an openCreateDialog
    callback — better React and no cascading render.
  • Gates re-run on the final HEAD: check:control-bytes, check:i18n-keys,
    check:i18n-drift, check:phantom-deps, check:self-import, changeset:check — all pass.
    check:i18n-keys confirms both new defaultValue strings match their en pack
    values and pass exactly the holes those values have; check:i18n-drift reports
    "2 keys added, 0 removed, 0 en values changed".

i18n — a note on "all ten locale packs"

This repo has two catalogs, and the dispatch's instruction applies to one of them:

  • preview/DraftChangesPanel.tsx reads @object-ui/i18n, so its two new keys went into
    all ten packs (ar, de, en, es, fr, ja, ko, pt, ru, zh).
  • The Studio pillars read views/metadata-admin/i18n.ts, which is a deliberate
    two-locale table (en-US | zh-CN). engine.studio.* appears 0 times across
    all ten packs — counter-probed, the packs are real and populated — so the
    create-dialog copy follows the existing convention there. Adding it to the ten-pack
    would have been dead weight.

Not flaky

Adding the block surfaced a latent race: it rendered the bare object name, which
collided with the existing suite's getByText('ticket'), resolving on whichever the
async lint won. Fixed by construction — the block addresses items as type/name
so the existing DraftChangesPanel.test.tsx is byte-for-byte untouched.

Out-of-scope findings


Generated by Claude Code

…created
Studio's 新建对象 collected exactly two fields (label, identifier) and the
skeleton it saved declared no `sharingModel`. The draft saved happily, the
form editor worked, and the object was then refused at 发布 → 全部发布 by
`security-owd-unset` — a required decision the surface never asked for,
delivered only by failing.
- `buildObjectSkeleton` now takes `sharingModel` as a REQUIRED parameter, so
no create path can omit the baseline without failing to type-check.
- The create dialog gains the choice, pre-selected to `private` and glossed
with the Settings tab's own strings. `controlled_by_parent` is deliberately
not offered: a just-created object has no master-detail relation for it to
derive access from.
- The Settings tab stops describing an unset OWD as safe. It is styled as the
publish-blocking problem it is, matching the D11 external-wider warning
beside it.
Part of #5418
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
…ot by it
The pending-changes sheet — the surface behind 发布 → 全部发布 — now runs the
framework's own `validateSecurityPosture` over the pending object drafts and
names any blocking finding, with its fix-it hint, beside the confirm button.
It mirrors the producer's rule rather than re-deriving it: a console-local "is
sharingModel missing" check would be a fork of a security gate, free to drift
from the door it claims to predict. It reports without blocking — the server
door stays the authority, and the installed lint can legitimately differ from
the version the server enforces.
The `import()` stays dynamic on purpose: `@objectstack/lint` is the one
`@objectstack/*` package the console's vendor chunk group does not claim, so a
static import would pull the lint bundle onto the eager console graph.
Copy lands in all ten locale packs — this surface reads the `@object-ui/i18n`
catalog, unlike the Studio pillars, whose `engine.studio.*` catalog is a
two-locale table.
Part of #5418
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
…tems as type/name
Adds the positive coverage the previous commit lacked: the sheet names an
OWD-less object draft, keeps Publish enabled while doing so, and goes quiet
once the baseline is authored.
The block addresses an item as `object/crmext_visit` — the way the server's own
publish refusal addresses it, and, unlike a bare name, text that cannot collide
with the same draft's row in the list above. That collision was a live flake:
the existing suite's `getByText('ticket')` matched both the row and the block,
resolving on whichever the async lint won the race to. Fixed by construction, so
the existing suite is left untouched.
Part of #5418
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
…effect
The effect spelling re-rendered the dialog a second time on every open purely
to undo a previous session's pick, and tripped react-hooks/set-state-in-effect
— one warning over this file's origin/main baseline of 16. Routing both
openers through one `openCreateDialog` callback returns the file to 16.
Part of #5418
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 58 chunks)3792.4 KB3867.2 KB
Main entry chunk (gzip)25.3 KB350 KB
Entry fileindex-B1EpjxpN.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)7.42KB2.32KB
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.98KB113.63KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.34KB
fields (index.js)237.21KB59.50KB
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.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.37KB32.91KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.04KB60.86KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.40KB30.26KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.75KB54.24KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.64KB27.13KB
plugin-map (index.js)20.08KB6.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.52KB20.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)36.10KB12.26KB
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-support-ai
os-support-ai marked this pull request as ready for review August 21, 2026 02:13
@os-support-ai
os-support-ai added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 7a90afdAug 21, 2026
23 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5418-studio-new-object-sharingmodel branch August 21, 2026 02:14
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.

2 participants

@os-support-ai@claude