feat(studio): render the publish door's advisory findings - #6961

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories
Aug 31, 2026
Merged

feat(studio): render the publish door's advisory findings#6961
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5026

Studio's publish door discarded the runtime authoring gate's advisories. This wires them into the rendering path #4133 / PR #4236 already built for the save door — same component, second source, no new UI shape.

Session for this work: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB

Contract-review conditions, folded in

Both conditions from the ACCEPT-WITH-CONDITIONS verdict are addressed in 1c91abd98.

Condition 1 (blocking) — the changeset is regraded minor and the break is named. The required door member is reader-additive but constructor-breaking: a door-less event literal that type-checked before now fails TS2741. The review measured that on the emitted dist/index.d.ts on both sides, and it is the entire non-comment delta of the package's published surface. My grade of patch was wrong, and the defence I gave for it — "an interface that only this repo constructs" — was an in-repo census standing in for an unmeasurable out-of-repo fact; unmeasurable grades as present, and this PR's own fixture edits are the constructor pattern a consumer's tests would use. The @object-ui/data-objectstack entry now reads minor (every publishable package sits in one fixed group, so that entry carries the group), and the changeset body names the break with its one-line migration: add door: 'save' or door: 'publish', whichever write the literal models. Not major, deliberately — objectui's major is pinned to @objectstack's so that "same major means compatible" holds across the two repos, which is what scripts/check-changeset-no-major.mjs exists to enforce.

Condition 2 — the renderer's door handling is now exhaustive. Covered below, in the very section whose claim it corrects.

The gate measurement came first, and it could have ended the task

This card was held 13 days on a spec-pin condition, released on the observation that the stated reason had expired (the lockfile moved from 17.0.0-rc.2 to 17.2.0). A version number is not a key, so the first action here was reading the installed package, with a hot control.

Measured in this worktree, at runtime, against node_modules/@objectstack/spec — not against framework main, and not inferred from the version:

installed @objectstack/spec version: 17.2.0
--- PublishMetaItemResponseSchema [the card's key] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true
(advisories.0.path | advisories.0.where | advisories.0.message
| advisories.0.hint | advisories.0.severity)
--- SaveMetaItemResponseSchema [HOT CONTROL, carried since #4717] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true

Three things make that a reading rather than a shrug. Survival, not mere acceptance: an undeclared key is stripped by the object schema, so "parses fine" would prove nothing — the key had to come back out. The reverse probe: a half-shaped finding is rejected, so the key is genuinely validated rather than waved through. And the control: the sibling key on the save door answers identically, so a zero would have meant a broken instrument, not an absent key.

The gate opened. That measurement is now a test (metadata-client.publishAdvisories.test.ts, first describe block) rather than a line in a transcript, so a spec drift fails CI instead of silently re-muting the door.

⚠️ The card's account of the upstream change (objectstack#9176 / PR #9344, 09a6eeee8) is the filing seat's 2026-08-17 report, not my measurement. I did not verify the commit, the PR, or the attribution. What I verified is the state of the installed package, above.

What changed

MetadataClient.publish and MetadataClient.publishDraft — the two methods over the single-item publish route POST /meta/:type/:name/publish — now report through the same sink, the same event and the same renderer the save door already used.

The wiring lands in the data layer, not at the call sites, for the reason PR #4236 gave one door over: every app-shell write path takes its client from useMetadataClient, so one seam covers ResourceEditPage's Publish button (line 1507) and the runtime RuntimeDraftBar promotion behind ObjectView / ReportView / DashboardView, plus any future call site. Both clones (withEnvironment, withPreviewDrafts) already forwarded the sink, and pins cover both.

Why this door is the one that mattered. PR #4236 recorded the gap honestly: Studio's designer stages every edit as a mode: 'draft' save, drafts are never gated (the framework returns at its D1 early-return before a rule runs), and the promotion that is gated declared no advisories field. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. The second has now expired.

One thing had to differ: the verb

MetadataSaveAdvisoryEvent gains a required door of 'save' | 'publish', and the renderer picks console.publishAdvisoryTitle (added to all ten locale packs) accordingly.

This is not decoration. Save and Publish are two different buttons in this product, so a toast reading "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. And mode cannot answer the question: a direct active save and a draft promotion both report mode: 'publish', because both land the body in the active overlay. A pin asserts exactly that discriminating case.

Required rather than optional-with-a-default so a future third door cannot be wired without saying which one it is; an omitted discriminator would silently render the save wording. Everything else about the surface is unchanged — warning tier, 10s duration, per-finding rule + message + hint, server prose rendered verbatim.

And the renderer handles the union exhaustively (contract-review condition 2). Requiring door guarantees a constructor states a door; on its own it does not guarantee the renderer handles the one it was given. The title choice was a two-way ternary, so a third union member would have compiled at its constructor, declared itself honestly, and still rendered "Saved" — the exact silent-wrong-verb class door exists to kill, reintroduced one level up. It is now a switch with a never check, so a new member is a compile error. Its unreachable default throws rather than falling back to the save wording: both emitters wrap the sink in a try/catch that swallows, so the failure mode is "no toast" rather than a toast that misstates what just happened to the author's data. Two pins cover the runtime half; the compile-time half is tsc's.

⚠️On the clause-② tripwire: carrying advisories required no widening of any ObjectUI-side declared type. It flows through the existing shapes untouched — readSaveAdvisories takes unknown, publishDraft already returned an intersection with Record of string to unknown, and publish is generic in its return. door carries provenance, not advisories, so the tripwire as written was not tripped, and I flagged it rather than burying it.

⚠️ But the review found the letter of that tripwire narrower than its purpose, and it is right: dooris a published-surface delta, and a breaking one for constructors. My accompanying line — "additive on an interface that only this repo constructs" — was an in-repo census doing duty for an unmeasurable out-of-repo fact, and it is withdrawn; see the conditions section at the top for the grade that replaces it. Recorded here because a future dispatch tripwire should read "any change to an exported type", so its letter matches its purpose.

Scope: the batch door is untouched, and that absence is pinned

"Publish whole app" (POST /packages/:id/publish-drafts) still discards per-draft advisories server-side — objectstack#9343 remains open and unruled — and nothing here compensates for that from the client side. That route is reached by a bare fetch in usePublishAllDrafts and by apiJson in PackagesPage; neither goes through MetadataClient, and neither is modified.

A test pins the absence rather than leaving it to a reader's goodwill: a batch-shaped body carrying findings under published[] reaching publishDraft renders nothing. A later "helpful" traversal cannot be added without turning it red.

publishDraft itself is wired because it is the same single-item route as publish, with the same response schema — the orphan-draft fallback in usePublishAllDrafts calls it per item and each response genuinely carries the key. Wiring the door rather than the caller avoids a latent asymmetry inside one route. Nothing traverses a batch response.

Also left alone deliberately: the SDK's meta.publishItem. ObjectStackAdapter's interceptor wraps meta.saveItem only, and publishItem has zero callers in this repo (measured), so wiring it would be surface with no consumer.

Reverse verification

Direction predicted before running: red — removing the two publish-door emits restores parse-and-discard, so every pin asserting an event arrives fails.

Measured at final head 1c91abd98, exactly as predicted — 8 red / 24 green:

× emits the findings a successful promotion returned
× names the PUBLISH door, which `mode` alone cannot say
× carries rule, message and hint through verbatim — they are server prose
× drops half-shaped findings rather than rendering blanks at the author
× survives the withEnvironment clone — console clients are all env-scoped
× survives the withPreviewDrafts clone
× emits the findings a by-reference promotion returned
× reads them through the dispatcher `{ success, data }` envelope it already unwraps
Test Files 1 failed | 1 passed (2)
Tests 8 failed | 24 passed (32)

The mutation was confirmed on disk before the run — anchor count 2 to 0, blob hash 83af5ebd to 43884057 — and the restore was proven the same way rather than by an exit code: restored hash equals the HEAD blob 83af5ebd, git diff HEAD empty, anchors back to 2. Restored run: 32 passed (32).

An earlier revision of this body reported the same eight reds over 22 passed (30), measured at the previous head 3d73b1314. The totals are reconciled rather than merely replaced: this commit adds exactly two tests (the exhaustiveness pair above), and both counts moved by exactly two — the union from 1040 to 1042, and this two-file set from 30 to 32. The red set is identical in every run by either party. The head-accurate figure is the one printed above. No rebuild leg is claimed or required: the subject is reached by a relative source import (from './metadata-client'), so no dist is in the resolution path.

Worth recording, because it says which tests carry the wiring: saveAdvisoryToast.test.ts stayed fully green through the ablation. It exercises the pure builder over a hand-made event, so the publish-door pins in the data layer are what actually guard this — the same asymmetry PR #4236 recorded for the save door. The review took that note further and found the seam neither suite covers (useMetadata.ts lines 130-135, where the hook hands the sink to the factory: cut it and both doors go silent with every named suite green). Inherited from #4236, filed as #6969, not fixed here.

Tests

All at final commit 1c91abd98, union re-run after the last commit:

vitest (root-relative, 65 files) 65 passed, 1042 tests PASS
incl. metadata-client.publishAdvisories, metadata-client.saveAdvisories,
onSaveAdvisory, metadata-client, saveAdvisoryToast,
MetadataService.saveAdvisories, runtime-metadata-persistence,
packages/i18n (locale parity)
type-check data-objectstack / app-shell / i18n all "Done" PASS
check:control-bytes PASS
check:i18n-keys / check:i18n-drift / check:i18n-dead-keys PASS
check:spec-symbols PASS
check:vi-mock-specifiers / check:vi-mock-inherit PASS
changeset:check (check-changeset-fixed + no-major) PASS
check-changeset-overwrite / check-changeset-presence PASS
eslint (touched files, both rounds) 0 errors, warnings only

Every eslint warning is a pre-existing no-explicit-any in a touched file (133 in the first round over 17 files; the second round's two provider files are 0/0); this change adds none.

⚠️One gate is NOT MEASURED locally, and it is not a pass.check:readme-exports exits 1 in this worktree, but every one of its messages is type entry ./dist/index.d.ts is not on disk — run pnpm build first, naming packages the diff never touches. Only the dependency closure was built here, not the whole repo. The gate's own census reports its real verdict classes clean (0 wrong-path, 0 fabricated) and then declares its own population collapse, so the exit code is the unbuilt-tree prerequisite, not a finding. It has its own CI workflow and will be judged there on a fully built tree.

Declared narrowing: the repo-wide pnpm lint scan was not run locally; eslint was run over the touched files instead. CI runs the full farm regardless.

A method note, since it cost a run

The repo's vitest guard (objectui#3378) rejected pnpm --filter PKG exec vitest run …: launched from a package directory, vitest re-roots, the root projects match nothing, and it runs apps/console's 22 files reporting Test Files 22 passed (22) — a green that tested none of the package. Every number above comes from a root-relative invocation. The guard caught it; without it this PR would have shipped a fabricated green.


Generated by Claude Code

The runtime authoring gate reports on BOTH metadata write doors, but objectui
rendered only one of them. objectui#4133 wired the save door and recorded why
that left the common path silent: Studio's designer stages every edit as a
`mode: 'draft'` save, drafts are never gated (the framework returns at its D1
early-return before a rule runs), and the publish step that IS gated declared
no `advisories` field to carry the findings.
`PublishMetaItemResponseSchema` now declares that key (objectstack#9176), so
`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods
over `POST /meta/:type/:name/publish` — report through the same sink, event and
renderer the save door already used. The wiring lives in the data layer, so the
ResourceEditPage Publish button and the RuntimeDraftBar promotion are covered
by one change rather than a toast per call site.
`MetadataSaveAdvisoryEvent` gains a required `door: 'save' | 'publish'`: Save
and Publish are two different buttons here, so "Saved" after a Publish would
tell the author their change is still a draft. `mode` cannot answer this — a
direct active save and a draft promotion both report `mode: 'publish'`.
The batch door (`POST /packages/:id/publish-drafts`) still discards per-draft
advisories server-side and nothing here compensates for it; a test pins that
absence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.1 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-C5WkKps1.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

… exhaustively
Two conditions from the contract review on this branch.
Condition 1 (blocking). The required `door` member on the exported
`MetadataSaveAdvisoryEvent` is reader-additive but constructor-breaking: a
door-less event literal that type-checked before now fails with TS2741. It was
graded `patch`. Regraded to `minor` on the data-objectstack entry, which carries
the fixed group, with the break and its one-line migration named in the
changeset body. Never `major`: objectui's major is pinned to `@objectstack`'s so
that "same major means compatible" holds across the two repos, so objectui's own
breaking changes ship as `minor` with the break spelled out
(scripts/check-changeset-no-major.mjs).
Condition 2. `door` being required guarantees a constructor STATES a door; it
did not guarantee the renderer HANDLES the one it was given, because the title
choice was a two-way ternary. A third union member would have compiled at its
constructor, declared itself honestly, and still rendered "Saved" — the exact
silent-wrong-verb class `door` exists to kill, one level up. The choice is now
an exhaustive switch with a `never` check, so a new member is a compile error.
Its unreachable default throws rather than falling back to the save wording:
both emitters swallow, so the failure mode is "no toast" rather than a toast
that misstates what just happened to the author's data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-_x4gqw8_.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: render the publish door's advisories — the key #4133 scoped out now exists on PublishMetaItemResponse

2 participants

@os-sam@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" + '
Skip to content

feat(studio): render the publish door's advisory findings - #6961

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories
Aug 31, 2026
Merged

feat(studio): render the publish door's advisory findings#6961
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5026

Studio's publish door discarded the runtime authoring gate's advisories. This wires them into the rendering path #4133 / PR #4236 already built for the save door — same component, second source, no new UI shape.

Session for this work: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB

Contract-review conditions, folded in

Both conditions from the ACCEPT-WITH-CONDITIONS verdict are addressed in 1c91abd98.

Condition 1 (blocking) — the changeset is regraded minor and the break is named. The required door member is reader-additive but constructor-breaking: a door-less event literal that type-checked before now fails TS2741. The review measured that on the emitted dist/index.d.ts on both sides, and it is the entire non-comment delta of the package's published surface. My grade of patch was wrong, and the defence I gave for it — "an interface that only this repo constructs" — was an in-repo census standing in for an unmeasurable out-of-repo fact; unmeasurable grades as present, and this PR's own fixture edits are the constructor pattern a consumer's tests would use. The @object-ui/data-objectstack entry now reads minor (every publishable package sits in one fixed group, so that entry carries the group), and the changeset body names the break with its one-line migration: add door: 'save' or door: 'publish', whichever write the literal models. Not major, deliberately — objectui's major is pinned to @objectstack's so that "same major means compatible" holds across the two repos, which is what scripts/check-changeset-no-major.mjs exists to enforce.

Condition 2 — the renderer's door handling is now exhaustive. Covered below, in the very section whose claim it corrects.

The gate measurement came first, and it could have ended the task

This card was held 13 days on a spec-pin condition, released on the observation that the stated reason had expired (the lockfile moved from 17.0.0-rc.2 to 17.2.0). A version number is not a key, so the first action here was reading the installed package, with a hot control.

Measured in this worktree, at runtime, against node_modules/@objectstack/spec — not against framework main, and not inferred from the version:

installed @objectstack/spec version: 17.2.0
--- PublishMetaItemResponseSchema [the card's key] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true
(advisories.0.path | advisories.0.where | advisories.0.message
| advisories.0.hint | advisories.0.severity)
--- SaveMetaItemResponseSchema [HOT CONTROL, carried since #4717] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true

Three things make that a reading rather than a shrug. Survival, not mere acceptance: an undeclared key is stripped by the object schema, so "parses fine" would prove nothing — the key had to come back out. The reverse probe: a half-shaped finding is rejected, so the key is genuinely validated rather than waved through. And the control: the sibling key on the save door answers identically, so a zero would have meant a broken instrument, not an absent key.

The gate opened. That measurement is now a test (metadata-client.publishAdvisories.test.ts, first describe block) rather than a line in a transcript, so a spec drift fails CI instead of silently re-muting the door.

⚠️ The card's account of the upstream change (objectstack#9176 / PR #9344, 09a6eeee8) is the filing seat's 2026-08-17 report, not my measurement. I did not verify the commit, the PR, or the attribution. What I verified is the state of the installed package, above.

What changed

MetadataClient.publish and MetadataClient.publishDraft — the two methods over the single-item publish route POST /meta/:type/:name/publish — now report through the same sink, the same event and the same renderer the save door already used.

The wiring lands in the data layer, not at the call sites, for the reason PR #4236 gave one door over: every app-shell write path takes its client from useMetadataClient, so one seam covers ResourceEditPage's Publish button (line 1507) and the runtime RuntimeDraftBar promotion behind ObjectView / ReportView / DashboardView, plus any future call site. Both clones (withEnvironment, withPreviewDrafts) already forwarded the sink, and pins cover both.

Why this door is the one that mattered. PR #4236 recorded the gap honestly: Studio's designer stages every edit as a mode: 'draft' save, drafts are never gated (the framework returns at its D1 early-return before a rule runs), and the promotion that is gated declared no advisories field. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. The second has now expired.

One thing had to differ: the verb

MetadataSaveAdvisoryEvent gains a required door of 'save' | 'publish', and the renderer picks console.publishAdvisoryTitle (added to all ten locale packs) accordingly.

This is not decoration. Save and Publish are two different buttons in this product, so a toast reading "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. And mode cannot answer the question: a direct active save and a draft promotion both report mode: 'publish', because both land the body in the active overlay. A pin asserts exactly that discriminating case.

Required rather than optional-with-a-default so a future third door cannot be wired without saying which one it is; an omitted discriminator would silently render the save wording. Everything else about the surface is unchanged — warning tier, 10s duration, per-finding rule + message + hint, server prose rendered verbatim.

And the renderer handles the union exhaustively (contract-review condition 2). Requiring door guarantees a constructor states a door; on its own it does not guarantee the renderer handles the one it was given. The title choice was a two-way ternary, so a third union member would have compiled at its constructor, declared itself honestly, and still rendered "Saved" — the exact silent-wrong-verb class door exists to kill, reintroduced one level up. It is now a switch with a never check, so a new member is a compile error. Its unreachable default throws rather than falling back to the save wording: both emitters wrap the sink in a try/catch that swallows, so the failure mode is "no toast" rather than a toast that misstates what just happened to the author's data. Two pins cover the runtime half; the compile-time half is tsc's.

⚠️On the clause-② tripwire: carrying advisories required no widening of any ObjectUI-side declared type. It flows through the existing shapes untouched — readSaveAdvisories takes unknown, publishDraft already returned an intersection with Record of string to unknown, and publish is generic in its return. door carries provenance, not advisories, so the tripwire as written was not tripped, and I flagged it rather than burying it.

⚠️ But the review found the letter of that tripwire narrower than its purpose, and it is right: dooris a published-surface delta, and a breaking one for constructors. My accompanying line — "additive on an interface that only this repo constructs" — was an in-repo census doing duty for an unmeasurable out-of-repo fact, and it is withdrawn; see the conditions section at the top for the grade that replaces it. Recorded here because a future dispatch tripwire should read "any change to an exported type", so its letter matches its purpose.

Scope: the batch door is untouched, and that absence is pinned

"Publish whole app" (POST /packages/:id/publish-drafts) still discards per-draft advisories server-side — objectstack#9343 remains open and unruled — and nothing here compensates for that from the client side. That route is reached by a bare fetch in usePublishAllDrafts and by apiJson in PackagesPage; neither goes through MetadataClient, and neither is modified.

A test pins the absence rather than leaving it to a reader's goodwill: a batch-shaped body carrying findings under published[] reaching publishDraft renders nothing. A later "helpful" traversal cannot be added without turning it red.

publishDraft itself is wired because it is the same single-item route as publish, with the same response schema — the orphan-draft fallback in usePublishAllDrafts calls it per item and each response genuinely carries the key. Wiring the door rather than the caller avoids a latent asymmetry inside one route. Nothing traverses a batch response.

Also left alone deliberately: the SDK's meta.publishItem. ObjectStackAdapter's interceptor wraps meta.saveItem only, and publishItem has zero callers in this repo (measured), so wiring it would be surface with no consumer.

Reverse verification

Direction predicted before running: red — removing the two publish-door emits restores parse-and-discard, so every pin asserting an event arrives fails.

Measured at final head 1c91abd98, exactly as predicted — 8 red / 24 green:

× emits the findings a successful promotion returned
× names the PUBLISH door, which `mode` alone cannot say
× carries rule, message and hint through verbatim — they are server prose
× drops half-shaped findings rather than rendering blanks at the author
× survives the withEnvironment clone — console clients are all env-scoped
× survives the withPreviewDrafts clone
× emits the findings a by-reference promotion returned
× reads them through the dispatcher `{ success, data }` envelope it already unwraps
Test Files 1 failed | 1 passed (2)
Tests 8 failed | 24 passed (32)

The mutation was confirmed on disk before the run — anchor count 2 to 0, blob hash 83af5ebd to 43884057 — and the restore was proven the same way rather than by an exit code: restored hash equals the HEAD blob 83af5ebd, git diff HEAD empty, anchors back to 2. Restored run: 32 passed (32).

An earlier revision of this body reported the same eight reds over 22 passed (30), measured at the previous head 3d73b1314. The totals are reconciled rather than merely replaced: this commit adds exactly two tests (the exhaustiveness pair above), and both counts moved by exactly two — the union from 1040 to 1042, and this two-file set from 30 to 32. The red set is identical in every run by either party. The head-accurate figure is the one printed above. No rebuild leg is claimed or required: the subject is reached by a relative source import (from './metadata-client'), so no dist is in the resolution path.

Worth recording, because it says which tests carry the wiring: saveAdvisoryToast.test.ts stayed fully green through the ablation. It exercises the pure builder over a hand-made event, so the publish-door pins in the data layer are what actually guard this — the same asymmetry PR #4236 recorded for the save door. The review took that note further and found the seam neither suite covers (useMetadata.ts lines 130-135, where the hook hands the sink to the factory: cut it and both doors go silent with every named suite green). Inherited from #4236, filed as #6969, not fixed here.

Tests

All at final commit 1c91abd98, union re-run after the last commit:

vitest (root-relative, 65 files) 65 passed, 1042 tests PASS
incl. metadata-client.publishAdvisories, metadata-client.saveAdvisories,
onSaveAdvisory, metadata-client, saveAdvisoryToast,
MetadataService.saveAdvisories, runtime-metadata-persistence,
packages/i18n (locale parity)
type-check data-objectstack / app-shell / i18n all "Done" PASS
check:control-bytes PASS
check:i18n-keys / check:i18n-drift / check:i18n-dead-keys PASS
check:spec-symbols PASS
check:vi-mock-specifiers / check:vi-mock-inherit PASS
changeset:check (check-changeset-fixed + no-major) PASS
check-changeset-overwrite / check-changeset-presence PASS
eslint (touched files, both rounds) 0 errors, warnings only

Every eslint warning is a pre-existing no-explicit-any in a touched file (133 in the first round over 17 files; the second round's two provider files are 0/0); this change adds none.

⚠️One gate is NOT MEASURED locally, and it is not a pass.check:readme-exports exits 1 in this worktree, but every one of its messages is type entry ./dist/index.d.ts is not on disk — run pnpm build first, naming packages the diff never touches. Only the dependency closure was built here, not the whole repo. The gate's own census reports its real verdict classes clean (0 wrong-path, 0 fabricated) and then declares its own population collapse, so the exit code is the unbuilt-tree prerequisite, not a finding. It has its own CI workflow and will be judged there on a fully built tree.

Declared narrowing: the repo-wide pnpm lint scan was not run locally; eslint was run over the touched files instead. CI runs the full farm regardless.

A method note, since it cost a run

The repo's vitest guard (objectui#3378) rejected pnpm --filter PKG exec vitest run …: launched from a package directory, vitest re-roots, the root projects match nothing, and it runs apps/console's 22 files reporting Test Files 22 passed (22) — a green that tested none of the package. Every number above comes from a root-relative invocation. The guard caught it; without it this PR would have shipped a fabricated green.


Generated by Claude Code

The runtime authoring gate reports on BOTH metadata write doors, but objectui
rendered only one of them. objectui#4133 wired the save door and recorded why
that left the common path silent: Studio's designer stages every edit as a
`mode: 'draft'` save, drafts are never gated (the framework returns at its D1
early-return before a rule runs), and the publish step that IS gated declared
no `advisories` field to carry the findings.
`PublishMetaItemResponseSchema` now declares that key (objectstack#9176), so
`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods
over `POST /meta/:type/:name/publish` — report through the same sink, event and
renderer the save door already used. The wiring lives in the data layer, so the
ResourceEditPage Publish button and the RuntimeDraftBar promotion are covered
by one change rather than a toast per call site.
`MetadataSaveAdvisoryEvent` gains a required `door: 'save' | 'publish'`: Save
and Publish are two different buttons here, so "Saved" after a Publish would
tell the author their change is still a draft. `mode` cannot answer this — a
direct active save and a draft promotion both report `mode: 'publish'`.
The batch door (`POST /packages/:id/publish-drafts`) still discards per-draft
advisories server-side and nothing here compensates for it; a test pins that
absence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.1 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-C5WkKps1.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

… exhaustively
Two conditions from the contract review on this branch.
Condition 1 (blocking). The required `door` member on the exported
`MetadataSaveAdvisoryEvent` is reader-additive but constructor-breaking: a
door-less event literal that type-checked before now fails with TS2741. It was
graded `patch`. Regraded to `minor` on the data-objectstack entry, which carries
the fixed group, with the break and its one-line migration named in the
changeset body. Never `major`: objectui's major is pinned to `@objectstack`'s so
that "same major means compatible" holds across the two repos, so objectui's own
breaking changes ship as `minor` with the break spelled out
(scripts/check-changeset-no-major.mjs).
Condition 2. `door` being required guarantees a constructor STATES a door; it
did not guarantee the renderer HANDLES the one it was given, because the title
choice was a two-way ternary. A third union member would have compiled at its
constructor, declared itself honestly, and still rendered "Saved" — the exact
silent-wrong-verb class `door` exists to kill, one level up. The choice is now
an exhaustive switch with a `never` check, so a new member is a compile error.
Its unreachable default throws rather than falling back to the save wording:
both emitters swallow, so the failure mode is "no toast" rather than a toast
that misstates what just happened to the author's data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-_x4gqw8_.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: render the publish door's advisories — the key #4133 scoped out now exists on PublishMetaItemResponse

2 participants

@os-sam@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('^' + ".*" + '
Skip to content

feat(studio): render the publish door's advisory findings - #6961

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories
Aug 31, 2026
Merged

feat(studio): render the publish door's advisory findings#6961
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5026

Studio's publish door discarded the runtime authoring gate's advisories. This wires them into the rendering path #4133 / PR #4236 already built for the save door — same component, second source, no new UI shape.

Session for this work: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB

Contract-review conditions, folded in

Both conditions from the ACCEPT-WITH-CONDITIONS verdict are addressed in 1c91abd98.

Condition 1 (blocking) — the changeset is regraded minor and the break is named. The required door member is reader-additive but constructor-breaking: a door-less event literal that type-checked before now fails TS2741. The review measured that on the emitted dist/index.d.ts on both sides, and it is the entire non-comment delta of the package's published surface. My grade of patch was wrong, and the defence I gave for it — "an interface that only this repo constructs" — was an in-repo census standing in for an unmeasurable out-of-repo fact; unmeasurable grades as present, and this PR's own fixture edits are the constructor pattern a consumer's tests would use. The @object-ui/data-objectstack entry now reads minor (every publishable package sits in one fixed group, so that entry carries the group), and the changeset body names the break with its one-line migration: add door: 'save' or door: 'publish', whichever write the literal models. Not major, deliberately — objectui's major is pinned to @objectstack's so that "same major means compatible" holds across the two repos, which is what scripts/check-changeset-no-major.mjs exists to enforce.

Condition 2 — the renderer's door handling is now exhaustive. Covered below, in the very section whose claim it corrects.

The gate measurement came first, and it could have ended the task

This card was held 13 days on a spec-pin condition, released on the observation that the stated reason had expired (the lockfile moved from 17.0.0-rc.2 to 17.2.0). A version number is not a key, so the first action here was reading the installed package, with a hot control.

Measured in this worktree, at runtime, against node_modules/@objectstack/spec — not against framework main, and not inferred from the version:

installed @objectstack/spec version: 17.2.0
--- PublishMetaItemResponseSchema [the card's key] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true
(advisories.0.path | advisories.0.where | advisories.0.message
| advisories.0.hint | advisories.0.severity)
--- SaveMetaItemResponseSchema [HOT CONTROL, carried since #4717] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true

Three things make that a reading rather than a shrug. Survival, not mere acceptance: an undeclared key is stripped by the object schema, so "parses fine" would prove nothing — the key had to come back out. The reverse probe: a half-shaped finding is rejected, so the key is genuinely validated rather than waved through. And the control: the sibling key on the save door answers identically, so a zero would have meant a broken instrument, not an absent key.

The gate opened. That measurement is now a test (metadata-client.publishAdvisories.test.ts, first describe block) rather than a line in a transcript, so a spec drift fails CI instead of silently re-muting the door.

⚠️ The card's account of the upstream change (objectstack#9176 / PR #9344, 09a6eeee8) is the filing seat's 2026-08-17 report, not my measurement. I did not verify the commit, the PR, or the attribution. What I verified is the state of the installed package, above.

What changed

MetadataClient.publish and MetadataClient.publishDraft — the two methods over the single-item publish route POST /meta/:type/:name/publish — now report through the same sink, the same event and the same renderer the save door already used.

The wiring lands in the data layer, not at the call sites, for the reason PR #4236 gave one door over: every app-shell write path takes its client from useMetadataClient, so one seam covers ResourceEditPage's Publish button (line 1507) and the runtime RuntimeDraftBar promotion behind ObjectView / ReportView / DashboardView, plus any future call site. Both clones (withEnvironment, withPreviewDrafts) already forwarded the sink, and pins cover both.

Why this door is the one that mattered. PR #4236 recorded the gap honestly: Studio's designer stages every edit as a mode: 'draft' save, drafts are never gated (the framework returns at its D1 early-return before a rule runs), and the promotion that is gated declared no advisories field. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. The second has now expired.

One thing had to differ: the verb

MetadataSaveAdvisoryEvent gains a required door of 'save' | 'publish', and the renderer picks console.publishAdvisoryTitle (added to all ten locale packs) accordingly.

This is not decoration. Save and Publish are two different buttons in this product, so a toast reading "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. And mode cannot answer the question: a direct active save and a draft promotion both report mode: 'publish', because both land the body in the active overlay. A pin asserts exactly that discriminating case.

Required rather than optional-with-a-default so a future third door cannot be wired without saying which one it is; an omitted discriminator would silently render the save wording. Everything else about the surface is unchanged — warning tier, 10s duration, per-finding rule + message + hint, server prose rendered verbatim.

And the renderer handles the union exhaustively (contract-review condition 2). Requiring door guarantees a constructor states a door; on its own it does not guarantee the renderer handles the one it was given. The title choice was a two-way ternary, so a third union member would have compiled at its constructor, declared itself honestly, and still rendered "Saved" — the exact silent-wrong-verb class door exists to kill, reintroduced one level up. It is now a switch with a never check, so a new member is a compile error. Its unreachable default throws rather than falling back to the save wording: both emitters wrap the sink in a try/catch that swallows, so the failure mode is "no toast" rather than a toast that misstates what just happened to the author's data. Two pins cover the runtime half; the compile-time half is tsc's.

⚠️On the clause-② tripwire: carrying advisories required no widening of any ObjectUI-side declared type. It flows through the existing shapes untouched — readSaveAdvisories takes unknown, publishDraft already returned an intersection with Record of string to unknown, and publish is generic in its return. door carries provenance, not advisories, so the tripwire as written was not tripped, and I flagged it rather than burying it.

⚠️ But the review found the letter of that tripwire narrower than its purpose, and it is right: dooris a published-surface delta, and a breaking one for constructors. My accompanying line — "additive on an interface that only this repo constructs" — was an in-repo census doing duty for an unmeasurable out-of-repo fact, and it is withdrawn; see the conditions section at the top for the grade that replaces it. Recorded here because a future dispatch tripwire should read "any change to an exported type", so its letter matches its purpose.

Scope: the batch door is untouched, and that absence is pinned

"Publish whole app" (POST /packages/:id/publish-drafts) still discards per-draft advisories server-side — objectstack#9343 remains open and unruled — and nothing here compensates for that from the client side. That route is reached by a bare fetch in usePublishAllDrafts and by apiJson in PackagesPage; neither goes through MetadataClient, and neither is modified.

A test pins the absence rather than leaving it to a reader's goodwill: a batch-shaped body carrying findings under published[] reaching publishDraft renders nothing. A later "helpful" traversal cannot be added without turning it red.

publishDraft itself is wired because it is the same single-item route as publish, with the same response schema — the orphan-draft fallback in usePublishAllDrafts calls it per item and each response genuinely carries the key. Wiring the door rather than the caller avoids a latent asymmetry inside one route. Nothing traverses a batch response.

Also left alone deliberately: the SDK's meta.publishItem. ObjectStackAdapter's interceptor wraps meta.saveItem only, and publishItem has zero callers in this repo (measured), so wiring it would be surface with no consumer.

Reverse verification

Direction predicted before running: red — removing the two publish-door emits restores parse-and-discard, so every pin asserting an event arrives fails.

Measured at final head 1c91abd98, exactly as predicted — 8 red / 24 green:

× emits the findings a successful promotion returned
× names the PUBLISH door, which `mode` alone cannot say
× carries rule, message and hint through verbatim — they are server prose
× drops half-shaped findings rather than rendering blanks at the author
× survives the withEnvironment clone — console clients are all env-scoped
× survives the withPreviewDrafts clone
× emits the findings a by-reference promotion returned
× reads them through the dispatcher `{ success, data }` envelope it already unwraps
Test Files 1 failed | 1 passed (2)
Tests 8 failed | 24 passed (32)

The mutation was confirmed on disk before the run — anchor count 2 to 0, blob hash 83af5ebd to 43884057 — and the restore was proven the same way rather than by an exit code: restored hash equals the HEAD blob 83af5ebd, git diff HEAD empty, anchors back to 2. Restored run: 32 passed (32).

An earlier revision of this body reported the same eight reds over 22 passed (30), measured at the previous head 3d73b1314. The totals are reconciled rather than merely replaced: this commit adds exactly two tests (the exhaustiveness pair above), and both counts moved by exactly two — the union from 1040 to 1042, and this two-file set from 30 to 32. The red set is identical in every run by either party. The head-accurate figure is the one printed above. No rebuild leg is claimed or required: the subject is reached by a relative source import (from './metadata-client'), so no dist is in the resolution path.

Worth recording, because it says which tests carry the wiring: saveAdvisoryToast.test.ts stayed fully green through the ablation. It exercises the pure builder over a hand-made event, so the publish-door pins in the data layer are what actually guard this — the same asymmetry PR #4236 recorded for the save door. The review took that note further and found the seam neither suite covers (useMetadata.ts lines 130-135, where the hook hands the sink to the factory: cut it and both doors go silent with every named suite green). Inherited from #4236, filed as #6969, not fixed here.

Tests

All at final commit 1c91abd98, union re-run after the last commit:

vitest (root-relative, 65 files) 65 passed, 1042 tests PASS
incl. metadata-client.publishAdvisories, metadata-client.saveAdvisories,
onSaveAdvisory, metadata-client, saveAdvisoryToast,
MetadataService.saveAdvisories, runtime-metadata-persistence,
packages/i18n (locale parity)
type-check data-objectstack / app-shell / i18n all "Done" PASS
check:control-bytes PASS
check:i18n-keys / check:i18n-drift / check:i18n-dead-keys PASS
check:spec-symbols PASS
check:vi-mock-specifiers / check:vi-mock-inherit PASS
changeset:check (check-changeset-fixed + no-major) PASS
check-changeset-overwrite / check-changeset-presence PASS
eslint (touched files, both rounds) 0 errors, warnings only

Every eslint warning is a pre-existing no-explicit-any in a touched file (133 in the first round over 17 files; the second round's two provider files are 0/0); this change adds none.

⚠️One gate is NOT MEASURED locally, and it is not a pass.check:readme-exports exits 1 in this worktree, but every one of its messages is type entry ./dist/index.d.ts is not on disk — run pnpm build first, naming packages the diff never touches. Only the dependency closure was built here, not the whole repo. The gate's own census reports its real verdict classes clean (0 wrong-path, 0 fabricated) and then declares its own population collapse, so the exit code is the unbuilt-tree prerequisite, not a finding. It has its own CI workflow and will be judged there on a fully built tree.

Declared narrowing: the repo-wide pnpm lint scan was not run locally; eslint was run over the touched files instead. CI runs the full farm regardless.

A method note, since it cost a run

The repo's vitest guard (objectui#3378) rejected pnpm --filter PKG exec vitest run …: launched from a package directory, vitest re-roots, the root projects match nothing, and it runs apps/console's 22 files reporting Test Files 22 passed (22) — a green that tested none of the package. Every number above comes from a root-relative invocation. The guard caught it; without it this PR would have shipped a fabricated green.


Generated by Claude Code

The runtime authoring gate reports on BOTH metadata write doors, but objectui
rendered only one of them. objectui#4133 wired the save door and recorded why
that left the common path silent: Studio's designer stages every edit as a
`mode: 'draft'` save, drafts are never gated (the framework returns at its D1
early-return before a rule runs), and the publish step that IS gated declared
no `advisories` field to carry the findings.
`PublishMetaItemResponseSchema` now declares that key (objectstack#9176), so
`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods
over `POST /meta/:type/:name/publish` — report through the same sink, event and
renderer the save door already used. The wiring lives in the data layer, so the
ResourceEditPage Publish button and the RuntimeDraftBar promotion are covered
by one change rather than a toast per call site.
`MetadataSaveAdvisoryEvent` gains a required `door: 'save' | 'publish'`: Save
and Publish are two different buttons here, so "Saved" after a Publish would
tell the author their change is still a draft. `mode` cannot answer this — a
direct active save and a draft promotion both report `mode: 'publish'`.
The batch door (`POST /packages/:id/publish-drafts`) still discards per-draft
advisories server-side and nothing here compensates for it; a test pins that
absence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.1 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-C5WkKps1.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

… exhaustively
Two conditions from the contract review on this branch.
Condition 1 (blocking). The required `door` member on the exported
`MetadataSaveAdvisoryEvent` is reader-additive but constructor-breaking: a
door-less event literal that type-checked before now fails with TS2741. It was
graded `patch`. Regraded to `minor` on the data-objectstack entry, which carries
the fixed group, with the break and its one-line migration named in the
changeset body. Never `major`: objectui's major is pinned to `@objectstack`'s so
that "same major means compatible" holds across the two repos, so objectui's own
breaking changes ship as `minor` with the break spelled out
(scripts/check-changeset-no-major.mjs).
Condition 2. `door` being required guarantees a constructor STATES a door; it
did not guarantee the renderer HANDLES the one it was given, because the title
choice was a two-way ternary. A third union member would have compiled at its
constructor, declared itself honestly, and still rendered "Saved" — the exact
silent-wrong-verb class `door` exists to kill, one level up. The choice is now
an exhaustive switch with a `never` check, so a new member is a compile error.
Its unreachable default throws rather than falling back to the save wording:
both emitters swallow, so the failure mode is "no toast" rather than a toast
that misstates what just happened to the author's data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-_x4gqw8_.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: render the publish door's advisories — the key #4133 scoped out now exists on PublishMetaItemResponse

2 participants

@os-sam@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('^' + ".*" + '
Skip to content

feat(studio): render the publish door's advisory findings - #6961

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories
Aug 31, 2026
Merged

feat(studio): render the publish door's advisory findings#6961
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5026

Studio's publish door discarded the runtime authoring gate's advisories. This wires them into the rendering path #4133 / PR #4236 already built for the save door — same component, second source, no new UI shape.

Session for this work: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB

Contract-review conditions, folded in

Both conditions from the ACCEPT-WITH-CONDITIONS verdict are addressed in 1c91abd98.

Condition 1 (blocking) — the changeset is regraded minor and the break is named. The required door member is reader-additive but constructor-breaking: a door-less event literal that type-checked before now fails TS2741. The review measured that on the emitted dist/index.d.ts on both sides, and it is the entire non-comment delta of the package's published surface. My grade of patch was wrong, and the defence I gave for it — "an interface that only this repo constructs" — was an in-repo census standing in for an unmeasurable out-of-repo fact; unmeasurable grades as present, and this PR's own fixture edits are the constructor pattern a consumer's tests would use. The @object-ui/data-objectstack entry now reads minor (every publishable package sits in one fixed group, so that entry carries the group), and the changeset body names the break with its one-line migration: add door: 'save' or door: 'publish', whichever write the literal models. Not major, deliberately — objectui's major is pinned to @objectstack's so that "same major means compatible" holds across the two repos, which is what scripts/check-changeset-no-major.mjs exists to enforce.

Condition 2 — the renderer's door handling is now exhaustive. Covered below, in the very section whose claim it corrects.

The gate measurement came first, and it could have ended the task

This card was held 13 days on a spec-pin condition, released on the observation that the stated reason had expired (the lockfile moved from 17.0.0-rc.2 to 17.2.0). A version number is not a key, so the first action here was reading the installed package, with a hot control.

Measured in this worktree, at runtime, against node_modules/@objectstack/spec — not against framework main, and not inferred from the version:

installed @objectstack/spec version: 17.2.0
--- PublishMetaItemResponseSchema [the card's key] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true
(advisories.0.path | advisories.0.where | advisories.0.message
| advisories.0.hint | advisories.0.severity)
--- SaveMetaItemResponseSchema [HOT CONTROL, carried since #4717] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true

Three things make that a reading rather than a shrug. Survival, not mere acceptance: an undeclared key is stripped by the object schema, so "parses fine" would prove nothing — the key had to come back out. The reverse probe: a half-shaped finding is rejected, so the key is genuinely validated rather than waved through. And the control: the sibling key on the save door answers identically, so a zero would have meant a broken instrument, not an absent key.

The gate opened. That measurement is now a test (metadata-client.publishAdvisories.test.ts, first describe block) rather than a line in a transcript, so a spec drift fails CI instead of silently re-muting the door.

⚠️ The card's account of the upstream change (objectstack#9176 / PR #9344, 09a6eeee8) is the filing seat's 2026-08-17 report, not my measurement. I did not verify the commit, the PR, or the attribution. What I verified is the state of the installed package, above.

What changed

MetadataClient.publish and MetadataClient.publishDraft — the two methods over the single-item publish route POST /meta/:type/:name/publish — now report through the same sink, the same event and the same renderer the save door already used.

The wiring lands in the data layer, not at the call sites, for the reason PR #4236 gave one door over: every app-shell write path takes its client from useMetadataClient, so one seam covers ResourceEditPage's Publish button (line 1507) and the runtime RuntimeDraftBar promotion behind ObjectView / ReportView / DashboardView, plus any future call site. Both clones (withEnvironment, withPreviewDrafts) already forwarded the sink, and pins cover both.

Why this door is the one that mattered. PR #4236 recorded the gap honestly: Studio's designer stages every edit as a mode: 'draft' save, drafts are never gated (the framework returns at its D1 early-return before a rule runs), and the promotion that is gated declared no advisories field. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. The second has now expired.

One thing had to differ: the verb

MetadataSaveAdvisoryEvent gains a required door of 'save' | 'publish', and the renderer picks console.publishAdvisoryTitle (added to all ten locale packs) accordingly.

This is not decoration. Save and Publish are two different buttons in this product, so a toast reading "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. And mode cannot answer the question: a direct active save and a draft promotion both report mode: 'publish', because both land the body in the active overlay. A pin asserts exactly that discriminating case.

Required rather than optional-with-a-default so a future third door cannot be wired without saying which one it is; an omitted discriminator would silently render the save wording. Everything else about the surface is unchanged — warning tier, 10s duration, per-finding rule + message + hint, server prose rendered verbatim.

And the renderer handles the union exhaustively (contract-review condition 2). Requiring door guarantees a constructor states a door; on its own it does not guarantee the renderer handles the one it was given. The title choice was a two-way ternary, so a third union member would have compiled at its constructor, declared itself honestly, and still rendered "Saved" — the exact silent-wrong-verb class door exists to kill, reintroduced one level up. It is now a switch with a never check, so a new member is a compile error. Its unreachable default throws rather than falling back to the save wording: both emitters wrap the sink in a try/catch that swallows, so the failure mode is "no toast" rather than a toast that misstates what just happened to the author's data. Two pins cover the runtime half; the compile-time half is tsc's.

⚠️On the clause-② tripwire: carrying advisories required no widening of any ObjectUI-side declared type. It flows through the existing shapes untouched — readSaveAdvisories takes unknown, publishDraft already returned an intersection with Record of string to unknown, and publish is generic in its return. door carries provenance, not advisories, so the tripwire as written was not tripped, and I flagged it rather than burying it.

⚠️ But the review found the letter of that tripwire narrower than its purpose, and it is right: dooris a published-surface delta, and a breaking one for constructors. My accompanying line — "additive on an interface that only this repo constructs" — was an in-repo census doing duty for an unmeasurable out-of-repo fact, and it is withdrawn; see the conditions section at the top for the grade that replaces it. Recorded here because a future dispatch tripwire should read "any change to an exported type", so its letter matches its purpose.

Scope: the batch door is untouched, and that absence is pinned

"Publish whole app" (POST /packages/:id/publish-drafts) still discards per-draft advisories server-side — objectstack#9343 remains open and unruled — and nothing here compensates for that from the client side. That route is reached by a bare fetch in usePublishAllDrafts and by apiJson in PackagesPage; neither goes through MetadataClient, and neither is modified.

A test pins the absence rather than leaving it to a reader's goodwill: a batch-shaped body carrying findings under published[] reaching publishDraft renders nothing. A later "helpful" traversal cannot be added without turning it red.

publishDraft itself is wired because it is the same single-item route as publish, with the same response schema — the orphan-draft fallback in usePublishAllDrafts calls it per item and each response genuinely carries the key. Wiring the door rather than the caller avoids a latent asymmetry inside one route. Nothing traverses a batch response.

Also left alone deliberately: the SDK's meta.publishItem. ObjectStackAdapter's interceptor wraps meta.saveItem only, and publishItem has zero callers in this repo (measured), so wiring it would be surface with no consumer.

Reverse verification

Direction predicted before running: red — removing the two publish-door emits restores parse-and-discard, so every pin asserting an event arrives fails.

Measured at final head 1c91abd98, exactly as predicted — 8 red / 24 green:

× emits the findings a successful promotion returned
× names the PUBLISH door, which `mode` alone cannot say
× carries rule, message and hint through verbatim — they are server prose
× drops half-shaped findings rather than rendering blanks at the author
× survives the withEnvironment clone — console clients are all env-scoped
× survives the withPreviewDrafts clone
× emits the findings a by-reference promotion returned
× reads them through the dispatcher `{ success, data }` envelope it already unwraps
Test Files 1 failed | 1 passed (2)
Tests 8 failed | 24 passed (32)

The mutation was confirmed on disk before the run — anchor count 2 to 0, blob hash 83af5ebd to 43884057 — and the restore was proven the same way rather than by an exit code: restored hash equals the HEAD blob 83af5ebd, git diff HEAD empty, anchors back to 2. Restored run: 32 passed (32).

An earlier revision of this body reported the same eight reds over 22 passed (30), measured at the previous head 3d73b1314. The totals are reconciled rather than merely replaced: this commit adds exactly two tests (the exhaustiveness pair above), and both counts moved by exactly two — the union from 1040 to 1042, and this two-file set from 30 to 32. The red set is identical in every run by either party. The head-accurate figure is the one printed above. No rebuild leg is claimed or required: the subject is reached by a relative source import (from './metadata-client'), so no dist is in the resolution path.

Worth recording, because it says which tests carry the wiring: saveAdvisoryToast.test.ts stayed fully green through the ablation. It exercises the pure builder over a hand-made event, so the publish-door pins in the data layer are what actually guard this — the same asymmetry PR #4236 recorded for the save door. The review took that note further and found the seam neither suite covers (useMetadata.ts lines 130-135, where the hook hands the sink to the factory: cut it and both doors go silent with every named suite green). Inherited from #4236, filed as #6969, not fixed here.

Tests

All at final commit 1c91abd98, union re-run after the last commit:

vitest (root-relative, 65 files) 65 passed, 1042 tests PASS
incl. metadata-client.publishAdvisories, metadata-client.saveAdvisories,
onSaveAdvisory, metadata-client, saveAdvisoryToast,
MetadataService.saveAdvisories, runtime-metadata-persistence,
packages/i18n (locale parity)
type-check data-objectstack / app-shell / i18n all "Done" PASS
check:control-bytes PASS
check:i18n-keys / check:i18n-drift / check:i18n-dead-keys PASS
check:spec-symbols PASS
check:vi-mock-specifiers / check:vi-mock-inherit PASS
changeset:check (check-changeset-fixed + no-major) PASS
check-changeset-overwrite / check-changeset-presence PASS
eslint (touched files, both rounds) 0 errors, warnings only

Every eslint warning is a pre-existing no-explicit-any in a touched file (133 in the first round over 17 files; the second round's two provider files are 0/0); this change adds none.

⚠️One gate is NOT MEASURED locally, and it is not a pass.check:readme-exports exits 1 in this worktree, but every one of its messages is type entry ./dist/index.d.ts is not on disk — run pnpm build first, naming packages the diff never touches. Only the dependency closure was built here, not the whole repo. The gate's own census reports its real verdict classes clean (0 wrong-path, 0 fabricated) and then declares its own population collapse, so the exit code is the unbuilt-tree prerequisite, not a finding. It has its own CI workflow and will be judged there on a fully built tree.

Declared narrowing: the repo-wide pnpm lint scan was not run locally; eslint was run over the touched files instead. CI runs the full farm regardless.

A method note, since it cost a run

The repo's vitest guard (objectui#3378) rejected pnpm --filter PKG exec vitest run …: launched from a package directory, vitest re-roots, the root projects match nothing, and it runs apps/console's 22 files reporting Test Files 22 passed (22) — a green that tested none of the package. Every number above comes from a root-relative invocation. The guard caught it; without it this PR would have shipped a fabricated green.


Generated by Claude Code

The runtime authoring gate reports on BOTH metadata write doors, but objectui
rendered only one of them. objectui#4133 wired the save door and recorded why
that left the common path silent: Studio's designer stages every edit as a
`mode: 'draft'` save, drafts are never gated (the framework returns at its D1
early-return before a rule runs), and the publish step that IS gated declared
no `advisories` field to carry the findings.
`PublishMetaItemResponseSchema` now declares that key (objectstack#9176), so
`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods
over `POST /meta/:type/:name/publish` — report through the same sink, event and
renderer the save door already used. The wiring lives in the data layer, so the
ResourceEditPage Publish button and the RuntimeDraftBar promotion are covered
by one change rather than a toast per call site.
`MetadataSaveAdvisoryEvent` gains a required `door: 'save' | 'publish'`: Save
and Publish are two different buttons here, so "Saved" after a Publish would
tell the author their change is still a draft. `mode` cannot answer this — a
direct active save and a draft promotion both report `mode: 'publish'`.
The batch door (`POST /packages/:id/publish-drafts`) still discards per-draft
advisories server-side and nothing here compensates for it; a test pins that
absence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.1 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-C5WkKps1.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

… exhaustively
Two conditions from the contract review on this branch.
Condition 1 (blocking). The required `door` member on the exported
`MetadataSaveAdvisoryEvent` is reader-additive but constructor-breaking: a
door-less event literal that type-checked before now fails with TS2741. It was
graded `patch`. Regraded to `minor` on the data-objectstack entry, which carries
the fixed group, with the break and its one-line migration named in the
changeset body. Never `major`: objectui's major is pinned to `@objectstack`'s so
that "same major means compatible" holds across the two repos, so objectui's own
breaking changes ship as `minor` with the break spelled out
(scripts/check-changeset-no-major.mjs).
Condition 2. `door` being required guarantees a constructor STATES a door; it
did not guarantee the renderer HANDLES the one it was given, because the title
choice was a two-way ternary. A third union member would have compiled at its
constructor, declared itself honestly, and still rendered "Saved" — the exact
silent-wrong-verb class `door` exists to kill, one level up. The choice is now
an exhaustive switch with a `never` check, so a new member is a compile error.
Its unreachable default throws rather than falling back to the save wording:
both emitters swallow, so the failure mode is "no toast" rather than a toast
that misstates what just happened to the author's data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-_x4gqw8_.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: render the publish door's advisories — the key #4133 scoped out now exists on PublishMetaItemResponse

2 participants

@os-sam@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" + '
Skip to content

feat(studio): render the publish door's advisory findings - #6961

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories
Aug 31, 2026
Merged

feat(studio): render the publish door's advisory findings#6961
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5026

Studio's publish door discarded the runtime authoring gate's advisories. This wires them into the rendering path #4133 / PR #4236 already built for the save door — same component, second source, no new UI shape.

Session for this work: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB

Contract-review conditions, folded in

Both conditions from the ACCEPT-WITH-CONDITIONS verdict are addressed in 1c91abd98.

Condition 1 (blocking) — the changeset is regraded minor and the break is named. The required door member is reader-additive but constructor-breaking: a door-less event literal that type-checked before now fails TS2741. The review measured that on the emitted dist/index.d.ts on both sides, and it is the entire non-comment delta of the package's published surface. My grade of patch was wrong, and the defence I gave for it — "an interface that only this repo constructs" — was an in-repo census standing in for an unmeasurable out-of-repo fact; unmeasurable grades as present, and this PR's own fixture edits are the constructor pattern a consumer's tests would use. The @object-ui/data-objectstack entry now reads minor (every publishable package sits in one fixed group, so that entry carries the group), and the changeset body names the break with its one-line migration: add door: 'save' or door: 'publish', whichever write the literal models. Not major, deliberately — objectui's major is pinned to @objectstack's so that "same major means compatible" holds across the two repos, which is what scripts/check-changeset-no-major.mjs exists to enforce.

Condition 2 — the renderer's door handling is now exhaustive. Covered below, in the very section whose claim it corrects.

The gate measurement came first, and it could have ended the task

This card was held 13 days on a spec-pin condition, released on the observation that the stated reason had expired (the lockfile moved from 17.0.0-rc.2 to 17.2.0). A version number is not a key, so the first action here was reading the installed package, with a hot control.

Measured in this worktree, at runtime, against node_modules/@objectstack/spec — not against framework main, and not inferred from the version:

installed @objectstack/spec version: 17.2.0
--- PublishMetaItemResponseSchema [the card's key] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true
(advisories.0.path | advisories.0.where | advisories.0.message
| advisories.0.hint | advisories.0.severity)
--- SaveMetaItemResponseSchema [HOT CONTROL, carried since #4717] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true

Three things make that a reading rather than a shrug. Survival, not mere acceptance: an undeclared key is stripped by the object schema, so "parses fine" would prove nothing — the key had to come back out. The reverse probe: a half-shaped finding is rejected, so the key is genuinely validated rather than waved through. And the control: the sibling key on the save door answers identically, so a zero would have meant a broken instrument, not an absent key.

The gate opened. That measurement is now a test (metadata-client.publishAdvisories.test.ts, first describe block) rather than a line in a transcript, so a spec drift fails CI instead of silently re-muting the door.

⚠️ The card's account of the upstream change (objectstack#9176 / PR #9344, 09a6eeee8) is the filing seat's 2026-08-17 report, not my measurement. I did not verify the commit, the PR, or the attribution. What I verified is the state of the installed package, above.

What changed

MetadataClient.publish and MetadataClient.publishDraft — the two methods over the single-item publish route POST /meta/:type/:name/publish — now report through the same sink, the same event and the same renderer the save door already used.

The wiring lands in the data layer, not at the call sites, for the reason PR #4236 gave one door over: every app-shell write path takes its client from useMetadataClient, so one seam covers ResourceEditPage's Publish button (line 1507) and the runtime RuntimeDraftBar promotion behind ObjectView / ReportView / DashboardView, plus any future call site. Both clones (withEnvironment, withPreviewDrafts) already forwarded the sink, and pins cover both.

Why this door is the one that mattered. PR #4236 recorded the gap honestly: Studio's designer stages every edit as a mode: 'draft' save, drafts are never gated (the framework returns at its D1 early-return before a rule runs), and the promotion that is gated declared no advisories field. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. The second has now expired.

One thing had to differ: the verb

MetadataSaveAdvisoryEvent gains a required door of 'save' | 'publish', and the renderer picks console.publishAdvisoryTitle (added to all ten locale packs) accordingly.

This is not decoration. Save and Publish are two different buttons in this product, so a toast reading "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. And mode cannot answer the question: a direct active save and a draft promotion both report mode: 'publish', because both land the body in the active overlay. A pin asserts exactly that discriminating case.

Required rather than optional-with-a-default so a future third door cannot be wired without saying which one it is; an omitted discriminator would silently render the save wording. Everything else about the surface is unchanged — warning tier, 10s duration, per-finding rule + message + hint, server prose rendered verbatim.

And the renderer handles the union exhaustively (contract-review condition 2). Requiring door guarantees a constructor states a door; on its own it does not guarantee the renderer handles the one it was given. The title choice was a two-way ternary, so a third union member would have compiled at its constructor, declared itself honestly, and still rendered "Saved" — the exact silent-wrong-verb class door exists to kill, reintroduced one level up. It is now a switch with a never check, so a new member is a compile error. Its unreachable default throws rather than falling back to the save wording: both emitters wrap the sink in a try/catch that swallows, so the failure mode is "no toast" rather than a toast that misstates what just happened to the author's data. Two pins cover the runtime half; the compile-time half is tsc's.

⚠️On the clause-② tripwire: carrying advisories required no widening of any ObjectUI-side declared type. It flows through the existing shapes untouched — readSaveAdvisories takes unknown, publishDraft already returned an intersection with Record of string to unknown, and publish is generic in its return. door carries provenance, not advisories, so the tripwire as written was not tripped, and I flagged it rather than burying it.

⚠️ But the review found the letter of that tripwire narrower than its purpose, and it is right: dooris a published-surface delta, and a breaking one for constructors. My accompanying line — "additive on an interface that only this repo constructs" — was an in-repo census doing duty for an unmeasurable out-of-repo fact, and it is withdrawn; see the conditions section at the top for the grade that replaces it. Recorded here because a future dispatch tripwire should read "any change to an exported type", so its letter matches its purpose.

Scope: the batch door is untouched, and that absence is pinned

"Publish whole app" (POST /packages/:id/publish-drafts) still discards per-draft advisories server-side — objectstack#9343 remains open and unruled — and nothing here compensates for that from the client side. That route is reached by a bare fetch in usePublishAllDrafts and by apiJson in PackagesPage; neither goes through MetadataClient, and neither is modified.

A test pins the absence rather than leaving it to a reader's goodwill: a batch-shaped body carrying findings under published[] reaching publishDraft renders nothing. A later "helpful" traversal cannot be added without turning it red.

publishDraft itself is wired because it is the same single-item route as publish, with the same response schema — the orphan-draft fallback in usePublishAllDrafts calls it per item and each response genuinely carries the key. Wiring the door rather than the caller avoids a latent asymmetry inside one route. Nothing traverses a batch response.

Also left alone deliberately: the SDK's meta.publishItem. ObjectStackAdapter's interceptor wraps meta.saveItem only, and publishItem has zero callers in this repo (measured), so wiring it would be surface with no consumer.

Reverse verification

Direction predicted before running: red — removing the two publish-door emits restores parse-and-discard, so every pin asserting an event arrives fails.

Measured at final head 1c91abd98, exactly as predicted — 8 red / 24 green:

× emits the findings a successful promotion returned
× names the PUBLISH door, which `mode` alone cannot say
× carries rule, message and hint through verbatim — they are server prose
× drops half-shaped findings rather than rendering blanks at the author
× survives the withEnvironment clone — console clients are all env-scoped
× survives the withPreviewDrafts clone
× emits the findings a by-reference promotion returned
× reads them through the dispatcher `{ success, data }` envelope it already unwraps
Test Files 1 failed | 1 passed (2)
Tests 8 failed | 24 passed (32)

The mutation was confirmed on disk before the run — anchor count 2 to 0, blob hash 83af5ebd to 43884057 — and the restore was proven the same way rather than by an exit code: restored hash equals the HEAD blob 83af5ebd, git diff HEAD empty, anchors back to 2. Restored run: 32 passed (32).

An earlier revision of this body reported the same eight reds over 22 passed (30), measured at the previous head 3d73b1314. The totals are reconciled rather than merely replaced: this commit adds exactly two tests (the exhaustiveness pair above), and both counts moved by exactly two — the union from 1040 to 1042, and this two-file set from 30 to 32. The red set is identical in every run by either party. The head-accurate figure is the one printed above. No rebuild leg is claimed or required: the subject is reached by a relative source import (from './metadata-client'), so no dist is in the resolution path.

Worth recording, because it says which tests carry the wiring: saveAdvisoryToast.test.ts stayed fully green through the ablation. It exercises the pure builder over a hand-made event, so the publish-door pins in the data layer are what actually guard this — the same asymmetry PR #4236 recorded for the save door. The review took that note further and found the seam neither suite covers (useMetadata.ts lines 130-135, where the hook hands the sink to the factory: cut it and both doors go silent with every named suite green). Inherited from #4236, filed as #6969, not fixed here.

Tests

All at final commit 1c91abd98, union re-run after the last commit:

vitest (root-relative, 65 files) 65 passed, 1042 tests PASS
incl. metadata-client.publishAdvisories, metadata-client.saveAdvisories,
onSaveAdvisory, metadata-client, saveAdvisoryToast,
MetadataService.saveAdvisories, runtime-metadata-persistence,
packages/i18n (locale parity)
type-check data-objectstack / app-shell / i18n all "Done" PASS
check:control-bytes PASS
check:i18n-keys / check:i18n-drift / check:i18n-dead-keys PASS
check:spec-symbols PASS
check:vi-mock-specifiers / check:vi-mock-inherit PASS
changeset:check (check-changeset-fixed + no-major) PASS
check-changeset-overwrite / check-changeset-presence PASS
eslint (touched files, both rounds) 0 errors, warnings only

Every eslint warning is a pre-existing no-explicit-any in a touched file (133 in the first round over 17 files; the second round's two provider files are 0/0); this change adds none.

⚠️One gate is NOT MEASURED locally, and it is not a pass.check:readme-exports exits 1 in this worktree, but every one of its messages is type entry ./dist/index.d.ts is not on disk — run pnpm build first, naming packages the diff never touches. Only the dependency closure was built here, not the whole repo. The gate's own census reports its real verdict classes clean (0 wrong-path, 0 fabricated) and then declares its own population collapse, so the exit code is the unbuilt-tree prerequisite, not a finding. It has its own CI workflow and will be judged there on a fully built tree.

Declared narrowing: the repo-wide pnpm lint scan was not run locally; eslint was run over the touched files instead. CI runs the full farm regardless.

A method note, since it cost a run

The repo's vitest guard (objectui#3378) rejected pnpm --filter PKG exec vitest run …: launched from a package directory, vitest re-roots, the root projects match nothing, and it runs apps/console's 22 files reporting Test Files 22 passed (22) — a green that tested none of the package. Every number above comes from a root-relative invocation. The guard caught it; without it this PR would have shipped a fabricated green.


Generated by Claude Code

The runtime authoring gate reports on BOTH metadata write doors, but objectui
rendered only one of them. objectui#4133 wired the save door and recorded why
that left the common path silent: Studio's designer stages every edit as a
`mode: 'draft'` save, drafts are never gated (the framework returns at its D1
early-return before a rule runs), and the publish step that IS gated declared
no `advisories` field to carry the findings.
`PublishMetaItemResponseSchema` now declares that key (objectstack#9176), so
`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods
over `POST /meta/:type/:name/publish` — report through the same sink, event and
renderer the save door already used. The wiring lives in the data layer, so the
ResourceEditPage Publish button and the RuntimeDraftBar promotion are covered
by one change rather than a toast per call site.
`MetadataSaveAdvisoryEvent` gains a required `door: 'save' | 'publish'`: Save
and Publish are two different buttons here, so "Saved" after a Publish would
tell the author their change is still a draft. `mode` cannot answer this — a
direct active save and a draft promotion both report `mode: 'publish'`.
The batch door (`POST /packages/:id/publish-drafts`) still discards per-draft
advisories server-side and nothing here compensates for it; a test pins that
absence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.1 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-C5WkKps1.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

… exhaustively
Two conditions from the contract review on this branch.
Condition 1 (blocking). The required `door` member on the exported
`MetadataSaveAdvisoryEvent` is reader-additive but constructor-breaking: a
door-less event literal that type-checked before now fails with TS2741. It was
graded `patch`. Regraded to `minor` on the data-objectstack entry, which carries
the fixed group, with the break and its one-line migration named in the
changeset body. Never `major`: objectui's major is pinned to `@objectstack`'s so
that "same major means compatible" holds across the two repos, so objectui's own
breaking changes ship as `minor` with the break spelled out
(scripts/check-changeset-no-major.mjs).
Condition 2. `door` being required guarantees a constructor STATES a door; it
did not guarantee the renderer HANDLES the one it was given, because the title
choice was a two-way ternary. A third union member would have compiled at its
constructor, declared itself honestly, and still rendered "Saved" — the exact
silent-wrong-verb class `door` exists to kill, one level up. The choice is now
an exhaustive switch with a `never` check, so a new member is a compile error.
Its unreachable default throws rather than falling back to the save wording:
both emitters swallow, so the failure mode is "no toast" rather than a toast
that misstates what just happened to the author's data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-_x4gqw8_.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: render the publish door's advisories — the key #4133 scoped out now exists on PublishMetaItemResponse

2 participants

@os-sam@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('^' + ".*" + '
Skip to content

feat(studio): render the publish door's advisory findings - #6961

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories
Aug 31, 2026
Merged

feat(studio): render the publish door's advisory findings#6961
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5026

Studio's publish door discarded the runtime authoring gate's advisories. This wires them into the rendering path #4133 / PR #4236 already built for the save door — same component, second source, no new UI shape.

Session for this work: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB

Contract-review conditions, folded in

Both conditions from the ACCEPT-WITH-CONDITIONS verdict are addressed in 1c91abd98.

Condition 1 (blocking) — the changeset is regraded minor and the break is named. The required door member is reader-additive but constructor-breaking: a door-less event literal that type-checked before now fails TS2741. The review measured that on the emitted dist/index.d.ts on both sides, and it is the entire non-comment delta of the package's published surface. My grade of patch was wrong, and the defence I gave for it — "an interface that only this repo constructs" — was an in-repo census standing in for an unmeasurable out-of-repo fact; unmeasurable grades as present, and this PR's own fixture edits are the constructor pattern a consumer's tests would use. The @object-ui/data-objectstack entry now reads minor (every publishable package sits in one fixed group, so that entry carries the group), and the changeset body names the break with its one-line migration: add door: 'save' or door: 'publish', whichever write the literal models. Not major, deliberately — objectui's major is pinned to @objectstack's so that "same major means compatible" holds across the two repos, which is what scripts/check-changeset-no-major.mjs exists to enforce.

Condition 2 — the renderer's door handling is now exhaustive. Covered below, in the very section whose claim it corrects.

The gate measurement came first, and it could have ended the task

This card was held 13 days on a spec-pin condition, released on the observation that the stated reason had expired (the lockfile moved from 17.0.0-rc.2 to 17.2.0). A version number is not a key, so the first action here was reading the installed package, with a hot control.

Measured in this worktree, at runtime, against node_modules/@objectstack/spec — not against framework main, and not inferred from the version:

installed @objectstack/spec version: 17.2.0
--- PublishMetaItemResponseSchema [the card's key] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true
(advisories.0.path | advisories.0.where | advisories.0.message
| advisories.0.hint | advisories.0.severity)
--- SaveMetaItemResponseSchema [HOT CONTROL, carried since #4717] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true

Three things make that a reading rather than a shrug. Survival, not mere acceptance: an undeclared key is stripped by the object schema, so "parses fine" would prove nothing — the key had to come back out. The reverse probe: a half-shaped finding is rejected, so the key is genuinely validated rather than waved through. And the control: the sibling key on the save door answers identically, so a zero would have meant a broken instrument, not an absent key.

The gate opened. That measurement is now a test (metadata-client.publishAdvisories.test.ts, first describe block) rather than a line in a transcript, so a spec drift fails CI instead of silently re-muting the door.

⚠️ The card's account of the upstream change (objectstack#9176 / PR #9344, 09a6eeee8) is the filing seat's 2026-08-17 report, not my measurement. I did not verify the commit, the PR, or the attribution. What I verified is the state of the installed package, above.

What changed

MetadataClient.publish and MetadataClient.publishDraft — the two methods over the single-item publish route POST /meta/:type/:name/publish — now report through the same sink, the same event and the same renderer the save door already used.

The wiring lands in the data layer, not at the call sites, for the reason PR #4236 gave one door over: every app-shell write path takes its client from useMetadataClient, so one seam covers ResourceEditPage's Publish button (line 1507) and the runtime RuntimeDraftBar promotion behind ObjectView / ReportView / DashboardView, plus any future call site. Both clones (withEnvironment, withPreviewDrafts) already forwarded the sink, and pins cover both.

Why this door is the one that mattered. PR #4236 recorded the gap honestly: Studio's designer stages every edit as a mode: 'draft' save, drafts are never gated (the framework returns at its D1 early-return before a rule runs), and the promotion that is gated declared no advisories field. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. The second has now expired.

One thing had to differ: the verb

MetadataSaveAdvisoryEvent gains a required door of 'save' | 'publish', and the renderer picks console.publishAdvisoryTitle (added to all ten locale packs) accordingly.

This is not decoration. Save and Publish are two different buttons in this product, so a toast reading "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. And mode cannot answer the question: a direct active save and a draft promotion both report mode: 'publish', because both land the body in the active overlay. A pin asserts exactly that discriminating case.

Required rather than optional-with-a-default so a future third door cannot be wired without saying which one it is; an omitted discriminator would silently render the save wording. Everything else about the surface is unchanged — warning tier, 10s duration, per-finding rule + message + hint, server prose rendered verbatim.

And the renderer handles the union exhaustively (contract-review condition 2). Requiring door guarantees a constructor states a door; on its own it does not guarantee the renderer handles the one it was given. The title choice was a two-way ternary, so a third union member would have compiled at its constructor, declared itself honestly, and still rendered "Saved" — the exact silent-wrong-verb class door exists to kill, reintroduced one level up. It is now a switch with a never check, so a new member is a compile error. Its unreachable default throws rather than falling back to the save wording: both emitters wrap the sink in a try/catch that swallows, so the failure mode is "no toast" rather than a toast that misstates what just happened to the author's data. Two pins cover the runtime half; the compile-time half is tsc's.

⚠️On the clause-② tripwire: carrying advisories required no widening of any ObjectUI-side declared type. It flows through the existing shapes untouched — readSaveAdvisories takes unknown, publishDraft already returned an intersection with Record of string to unknown, and publish is generic in its return. door carries provenance, not advisories, so the tripwire as written was not tripped, and I flagged it rather than burying it.

⚠️ But the review found the letter of that tripwire narrower than its purpose, and it is right: dooris a published-surface delta, and a breaking one for constructors. My accompanying line — "additive on an interface that only this repo constructs" — was an in-repo census doing duty for an unmeasurable out-of-repo fact, and it is withdrawn; see the conditions section at the top for the grade that replaces it. Recorded here because a future dispatch tripwire should read "any change to an exported type", so its letter matches its purpose.

Scope: the batch door is untouched, and that absence is pinned

"Publish whole app" (POST /packages/:id/publish-drafts) still discards per-draft advisories server-side — objectstack#9343 remains open and unruled — and nothing here compensates for that from the client side. That route is reached by a bare fetch in usePublishAllDrafts and by apiJson in PackagesPage; neither goes through MetadataClient, and neither is modified.

A test pins the absence rather than leaving it to a reader's goodwill: a batch-shaped body carrying findings under published[] reaching publishDraft renders nothing. A later "helpful" traversal cannot be added without turning it red.

publishDraft itself is wired because it is the same single-item route as publish, with the same response schema — the orphan-draft fallback in usePublishAllDrafts calls it per item and each response genuinely carries the key. Wiring the door rather than the caller avoids a latent asymmetry inside one route. Nothing traverses a batch response.

Also left alone deliberately: the SDK's meta.publishItem. ObjectStackAdapter's interceptor wraps meta.saveItem only, and publishItem has zero callers in this repo (measured), so wiring it would be surface with no consumer.

Reverse verification

Direction predicted before running: red — removing the two publish-door emits restores parse-and-discard, so every pin asserting an event arrives fails.

Measured at final head 1c91abd98, exactly as predicted — 8 red / 24 green:

× emits the findings a successful promotion returned
× names the PUBLISH door, which `mode` alone cannot say
× carries rule, message and hint through verbatim — they are server prose
× drops half-shaped findings rather than rendering blanks at the author
× survives the withEnvironment clone — console clients are all env-scoped
× survives the withPreviewDrafts clone
× emits the findings a by-reference promotion returned
× reads them through the dispatcher `{ success, data }` envelope it already unwraps
Test Files 1 failed | 1 passed (2)
Tests 8 failed | 24 passed (32)

The mutation was confirmed on disk before the run — anchor count 2 to 0, blob hash 83af5ebd to 43884057 — and the restore was proven the same way rather than by an exit code: restored hash equals the HEAD blob 83af5ebd, git diff HEAD empty, anchors back to 2. Restored run: 32 passed (32).

An earlier revision of this body reported the same eight reds over 22 passed (30), measured at the previous head 3d73b1314. The totals are reconciled rather than merely replaced: this commit adds exactly two tests (the exhaustiveness pair above), and both counts moved by exactly two — the union from 1040 to 1042, and this two-file set from 30 to 32. The red set is identical in every run by either party. The head-accurate figure is the one printed above. No rebuild leg is claimed or required: the subject is reached by a relative source import (from './metadata-client'), so no dist is in the resolution path.

Worth recording, because it says which tests carry the wiring: saveAdvisoryToast.test.ts stayed fully green through the ablation. It exercises the pure builder over a hand-made event, so the publish-door pins in the data layer are what actually guard this — the same asymmetry PR #4236 recorded for the save door. The review took that note further and found the seam neither suite covers (useMetadata.ts lines 130-135, where the hook hands the sink to the factory: cut it and both doors go silent with every named suite green). Inherited from #4236, filed as #6969, not fixed here.

Tests

All at final commit 1c91abd98, union re-run after the last commit:

vitest (root-relative, 65 files) 65 passed, 1042 tests PASS
incl. metadata-client.publishAdvisories, metadata-client.saveAdvisories,
onSaveAdvisory, metadata-client, saveAdvisoryToast,
MetadataService.saveAdvisories, runtime-metadata-persistence,
packages/i18n (locale parity)
type-check data-objectstack / app-shell / i18n all "Done" PASS
check:control-bytes PASS
check:i18n-keys / check:i18n-drift / check:i18n-dead-keys PASS
check:spec-symbols PASS
check:vi-mock-specifiers / check:vi-mock-inherit PASS
changeset:check (check-changeset-fixed + no-major) PASS
check-changeset-overwrite / check-changeset-presence PASS
eslint (touched files, both rounds) 0 errors, warnings only

Every eslint warning is a pre-existing no-explicit-any in a touched file (133 in the first round over 17 files; the second round's two provider files are 0/0); this change adds none.

⚠️One gate is NOT MEASURED locally, and it is not a pass.check:readme-exports exits 1 in this worktree, but every one of its messages is type entry ./dist/index.d.ts is not on disk — run pnpm build first, naming packages the diff never touches. Only the dependency closure was built here, not the whole repo. The gate's own census reports its real verdict classes clean (0 wrong-path, 0 fabricated) and then declares its own population collapse, so the exit code is the unbuilt-tree prerequisite, not a finding. It has its own CI workflow and will be judged there on a fully built tree.

Declared narrowing: the repo-wide pnpm lint scan was not run locally; eslint was run over the touched files instead. CI runs the full farm regardless.

A method note, since it cost a run

The repo's vitest guard (objectui#3378) rejected pnpm --filter PKG exec vitest run …: launched from a package directory, vitest re-roots, the root projects match nothing, and it runs apps/console's 22 files reporting Test Files 22 passed (22) — a green that tested none of the package. Every number above comes from a root-relative invocation. The guard caught it; without it this PR would have shipped a fabricated green.


Generated by Claude Code

The runtime authoring gate reports on BOTH metadata write doors, but objectui
rendered only one of them. objectui#4133 wired the save door and recorded why
that left the common path silent: Studio's designer stages every edit as a
`mode: 'draft'` save, drafts are never gated (the framework returns at its D1
early-return before a rule runs), and the publish step that IS gated declared
no `advisories` field to carry the findings.
`PublishMetaItemResponseSchema` now declares that key (objectstack#9176), so
`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods
over `POST /meta/:type/:name/publish` — report through the same sink, event and
renderer the save door already used. The wiring lives in the data layer, so the
ResourceEditPage Publish button and the RuntimeDraftBar promotion are covered
by one change rather than a toast per call site.
`MetadataSaveAdvisoryEvent` gains a required `door: 'save' | 'publish'`: Save
and Publish are two different buttons here, so "Saved" after a Publish would
tell the author their change is still a draft. `mode` cannot answer this — a
direct active save and a draft promotion both report `mode: 'publish'`.
The batch door (`POST /packages/:id/publish-drafts`) still discards per-draft
advisories server-side and nothing here compensates for it; a test pins that
absence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.1 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-C5WkKps1.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

… exhaustively
Two conditions from the contract review on this branch.
Condition 1 (blocking). The required `door` member on the exported
`MetadataSaveAdvisoryEvent` is reader-additive but constructor-breaking: a
door-less event literal that type-checked before now fails with TS2741. It was
graded `patch`. Regraded to `minor` on the data-objectstack entry, which carries
the fixed group, with the break and its one-line migration named in the
changeset body. Never `major`: objectui's major is pinned to `@objectstack`'s so
that "same major means compatible" holds across the two repos, so objectui's own
breaking changes ship as `minor` with the break spelled out
(scripts/check-changeset-no-major.mjs).
Condition 2. `door` being required guarantees a constructor STATES a door; it
did not guarantee the renderer HANDLES the one it was given, because the title
choice was a two-way ternary. A third union member would have compiled at its
constructor, declared itself honestly, and still rendered "Saved" — the exact
silent-wrong-verb class `door` exists to kill, one level up. The choice is now
an exhaustive switch with a `never` check, so a new member is a compile error.
Its unreachable default throws rather than falling back to the save wording:
both emitters swallow, so the failure mode is "no toast" rather than a toast
that misstates what just happened to the author's data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-_x4gqw8_.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: render the publish door's advisories — the key #4133 scoped out now exists on PublishMetaItemResponse

2 participants

@os-sam@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('^' + ".*" + '
Skip to content

feat(studio): render the publish door's advisory findings - #6961

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories
Aug 31, 2026
Merged

feat(studio): render the publish door's advisory findings#6961
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5026

Studio's publish door discarded the runtime authoring gate's advisories. This wires them into the rendering path #4133 / PR #4236 already built for the save door — same component, second source, no new UI shape.

Session for this work: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB

Contract-review conditions, folded in

Both conditions from the ACCEPT-WITH-CONDITIONS verdict are addressed in 1c91abd98.

Condition 1 (blocking) — the changeset is regraded minor and the break is named. The required door member is reader-additive but constructor-breaking: a door-less event literal that type-checked before now fails TS2741. The review measured that on the emitted dist/index.d.ts on both sides, and it is the entire non-comment delta of the package's published surface. My grade of patch was wrong, and the defence I gave for it — "an interface that only this repo constructs" — was an in-repo census standing in for an unmeasurable out-of-repo fact; unmeasurable grades as present, and this PR's own fixture edits are the constructor pattern a consumer's tests would use. The @object-ui/data-objectstack entry now reads minor (every publishable package sits in one fixed group, so that entry carries the group), and the changeset body names the break with its one-line migration: add door: 'save' or door: 'publish', whichever write the literal models. Not major, deliberately — objectui's major is pinned to @objectstack's so that "same major means compatible" holds across the two repos, which is what scripts/check-changeset-no-major.mjs exists to enforce.

Condition 2 — the renderer's door handling is now exhaustive. Covered below, in the very section whose claim it corrects.

The gate measurement came first, and it could have ended the task

This card was held 13 days on a spec-pin condition, released on the observation that the stated reason had expired (the lockfile moved from 17.0.0-rc.2 to 17.2.0). A version number is not a key, so the first action here was reading the installed package, with a hot control.

Measured in this worktree, at runtime, against node_modules/@objectstack/spec — not against framework main, and not inferred from the version:

installed @objectstack/spec version: 17.2.0
--- PublishMetaItemResponseSchema [the card's key] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true
(advisories.0.path | advisories.0.where | advisories.0.message
| advisories.0.hint | advisories.0.severity)
--- SaveMetaItemResponseSchema [HOT CONTROL, carried since #4717] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true

Three things make that a reading rather than a shrug. Survival, not mere acceptance: an undeclared key is stripped by the object schema, so "parses fine" would prove nothing — the key had to come back out. The reverse probe: a half-shaped finding is rejected, so the key is genuinely validated rather than waved through. And the control: the sibling key on the save door answers identically, so a zero would have meant a broken instrument, not an absent key.

The gate opened. That measurement is now a test (metadata-client.publishAdvisories.test.ts, first describe block) rather than a line in a transcript, so a spec drift fails CI instead of silently re-muting the door.

⚠️ The card's account of the upstream change (objectstack#9176 / PR #9344, 09a6eeee8) is the filing seat's 2026-08-17 report, not my measurement. I did not verify the commit, the PR, or the attribution. What I verified is the state of the installed package, above.

What changed

MetadataClient.publish and MetadataClient.publishDraft — the two methods over the single-item publish route POST /meta/:type/:name/publish — now report through the same sink, the same event and the same renderer the save door already used.

The wiring lands in the data layer, not at the call sites, for the reason PR #4236 gave one door over: every app-shell write path takes its client from useMetadataClient, so one seam covers ResourceEditPage's Publish button (line 1507) and the runtime RuntimeDraftBar promotion behind ObjectView / ReportView / DashboardView, plus any future call site. Both clones (withEnvironment, withPreviewDrafts) already forwarded the sink, and pins cover both.

Why this door is the one that mattered. PR #4236 recorded the gap honestly: Studio's designer stages every edit as a mode: 'draft' save, drafts are never gated (the framework returns at its D1 early-return before a rule runs), and the promotion that is gated declared no advisories field. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. The second has now expired.

One thing had to differ: the verb

MetadataSaveAdvisoryEvent gains a required door of 'save' | 'publish', and the renderer picks console.publishAdvisoryTitle (added to all ten locale packs) accordingly.

This is not decoration. Save and Publish are two different buttons in this product, so a toast reading "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. And mode cannot answer the question: a direct active save and a draft promotion both report mode: 'publish', because both land the body in the active overlay. A pin asserts exactly that discriminating case.

Required rather than optional-with-a-default so a future third door cannot be wired without saying which one it is; an omitted discriminator would silently render the save wording. Everything else about the surface is unchanged — warning tier, 10s duration, per-finding rule + message + hint, server prose rendered verbatim.

And the renderer handles the union exhaustively (contract-review condition 2). Requiring door guarantees a constructor states a door; on its own it does not guarantee the renderer handles the one it was given. The title choice was a two-way ternary, so a third union member would have compiled at its constructor, declared itself honestly, and still rendered "Saved" — the exact silent-wrong-verb class door exists to kill, reintroduced one level up. It is now a switch with a never check, so a new member is a compile error. Its unreachable default throws rather than falling back to the save wording: both emitters wrap the sink in a try/catch that swallows, so the failure mode is "no toast" rather than a toast that misstates what just happened to the author's data. Two pins cover the runtime half; the compile-time half is tsc's.

⚠️On the clause-② tripwire: carrying advisories required no widening of any ObjectUI-side declared type. It flows through the existing shapes untouched — readSaveAdvisories takes unknown, publishDraft already returned an intersection with Record of string to unknown, and publish is generic in its return. door carries provenance, not advisories, so the tripwire as written was not tripped, and I flagged it rather than burying it.

⚠️ But the review found the letter of that tripwire narrower than its purpose, and it is right: dooris a published-surface delta, and a breaking one for constructors. My accompanying line — "additive on an interface that only this repo constructs" — was an in-repo census doing duty for an unmeasurable out-of-repo fact, and it is withdrawn; see the conditions section at the top for the grade that replaces it. Recorded here because a future dispatch tripwire should read "any change to an exported type", so its letter matches its purpose.

Scope: the batch door is untouched, and that absence is pinned

"Publish whole app" (POST /packages/:id/publish-drafts) still discards per-draft advisories server-side — objectstack#9343 remains open and unruled — and nothing here compensates for that from the client side. That route is reached by a bare fetch in usePublishAllDrafts and by apiJson in PackagesPage; neither goes through MetadataClient, and neither is modified.

A test pins the absence rather than leaving it to a reader's goodwill: a batch-shaped body carrying findings under published[] reaching publishDraft renders nothing. A later "helpful" traversal cannot be added without turning it red.

publishDraft itself is wired because it is the same single-item route as publish, with the same response schema — the orphan-draft fallback in usePublishAllDrafts calls it per item and each response genuinely carries the key. Wiring the door rather than the caller avoids a latent asymmetry inside one route. Nothing traverses a batch response.

Also left alone deliberately: the SDK's meta.publishItem. ObjectStackAdapter's interceptor wraps meta.saveItem only, and publishItem has zero callers in this repo (measured), so wiring it would be surface with no consumer.

Reverse verification

Direction predicted before running: red — removing the two publish-door emits restores parse-and-discard, so every pin asserting an event arrives fails.

Measured at final head 1c91abd98, exactly as predicted — 8 red / 24 green:

× emits the findings a successful promotion returned
× names the PUBLISH door, which `mode` alone cannot say
× carries rule, message and hint through verbatim — they are server prose
× drops half-shaped findings rather than rendering blanks at the author
× survives the withEnvironment clone — console clients are all env-scoped
× survives the withPreviewDrafts clone
× emits the findings a by-reference promotion returned
× reads them through the dispatcher `{ success, data }` envelope it already unwraps
Test Files 1 failed | 1 passed (2)
Tests 8 failed | 24 passed (32)

The mutation was confirmed on disk before the run — anchor count 2 to 0, blob hash 83af5ebd to 43884057 — and the restore was proven the same way rather than by an exit code: restored hash equals the HEAD blob 83af5ebd, git diff HEAD empty, anchors back to 2. Restored run: 32 passed (32).

An earlier revision of this body reported the same eight reds over 22 passed (30), measured at the previous head 3d73b1314. The totals are reconciled rather than merely replaced: this commit adds exactly two tests (the exhaustiveness pair above), and both counts moved by exactly two — the union from 1040 to 1042, and this two-file set from 30 to 32. The red set is identical in every run by either party. The head-accurate figure is the one printed above. No rebuild leg is claimed or required: the subject is reached by a relative source import (from './metadata-client'), so no dist is in the resolution path.

Worth recording, because it says which tests carry the wiring: saveAdvisoryToast.test.ts stayed fully green through the ablation. It exercises the pure builder over a hand-made event, so the publish-door pins in the data layer are what actually guard this — the same asymmetry PR #4236 recorded for the save door. The review took that note further and found the seam neither suite covers (useMetadata.ts lines 130-135, where the hook hands the sink to the factory: cut it and both doors go silent with every named suite green). Inherited from #4236, filed as #6969, not fixed here.

Tests

All at final commit 1c91abd98, union re-run after the last commit:

vitest (root-relative, 65 files) 65 passed, 1042 tests PASS
incl. metadata-client.publishAdvisories, metadata-client.saveAdvisories,
onSaveAdvisory, metadata-client, saveAdvisoryToast,
MetadataService.saveAdvisories, runtime-metadata-persistence,
packages/i18n (locale parity)
type-check data-objectstack / app-shell / i18n all "Done" PASS
check:control-bytes PASS
check:i18n-keys / check:i18n-drift / check:i18n-dead-keys PASS
check:spec-symbols PASS
check:vi-mock-specifiers / check:vi-mock-inherit PASS
changeset:check (check-changeset-fixed + no-major) PASS
check-changeset-overwrite / check-changeset-presence PASS
eslint (touched files, both rounds) 0 errors, warnings only

Every eslint warning is a pre-existing no-explicit-any in a touched file (133 in the first round over 17 files; the second round's two provider files are 0/0); this change adds none.

⚠️One gate is NOT MEASURED locally, and it is not a pass.check:readme-exports exits 1 in this worktree, but every one of its messages is type entry ./dist/index.d.ts is not on disk — run pnpm build first, naming packages the diff never touches. Only the dependency closure was built here, not the whole repo. The gate's own census reports its real verdict classes clean (0 wrong-path, 0 fabricated) and then declares its own population collapse, so the exit code is the unbuilt-tree prerequisite, not a finding. It has its own CI workflow and will be judged there on a fully built tree.

Declared narrowing: the repo-wide pnpm lint scan was not run locally; eslint was run over the touched files instead. CI runs the full farm regardless.

A method note, since it cost a run

The repo's vitest guard (objectui#3378) rejected pnpm --filter PKG exec vitest run …: launched from a package directory, vitest re-roots, the root projects match nothing, and it runs apps/console's 22 files reporting Test Files 22 passed (22) — a green that tested none of the package. Every number above comes from a root-relative invocation. The guard caught it; without it this PR would have shipped a fabricated green.


Generated by Claude Code

The runtime authoring gate reports on BOTH metadata write doors, but objectui
rendered only one of them. objectui#4133 wired the save door and recorded why
that left the common path silent: Studio's designer stages every edit as a
`mode: 'draft'` save, drafts are never gated (the framework returns at its D1
early-return before a rule runs), and the publish step that IS gated declared
no `advisories` field to carry the findings.
`PublishMetaItemResponseSchema` now declares that key (objectstack#9176), so
`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods
over `POST /meta/:type/:name/publish` — report through the same sink, event and
renderer the save door already used. The wiring lives in the data layer, so the
ResourceEditPage Publish button and the RuntimeDraftBar promotion are covered
by one change rather than a toast per call site.
`MetadataSaveAdvisoryEvent` gains a required `door: 'save' | 'publish'`: Save
and Publish are two different buttons here, so "Saved" after a Publish would
tell the author their change is still a draft. `mode` cannot answer this — a
direct active save and a draft promotion both report `mode: 'publish'`.
The batch door (`POST /packages/:id/publish-drafts`) still discards per-draft
advisories server-side and nothing here compensates for it; a test pins that
absence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.1 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-C5WkKps1.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

… exhaustively
Two conditions from the contract review on this branch.
Condition 1 (blocking). The required `door` member on the exported
`MetadataSaveAdvisoryEvent` is reader-additive but constructor-breaking: a
door-less event literal that type-checked before now fails with TS2741. It was
graded `patch`. Regraded to `minor` on the data-objectstack entry, which carries
the fixed group, with the break and its one-line migration named in the
changeset body. Never `major`: objectui's major is pinned to `@objectstack`'s so
that "same major means compatible" holds across the two repos, so objectui's own
breaking changes ship as `minor` with the break spelled out
(scripts/check-changeset-no-major.mjs).
Condition 2. `door` being required guarantees a constructor STATES a door; it
did not guarantee the renderer HANDLES the one it was given, because the title
choice was a two-way ternary. A third union member would have compiled at its
constructor, declared itself honestly, and still rendered "Saved" — the exact
silent-wrong-verb class `door` exists to kill, one level up. The choice is now
an exhaustive switch with a `never` check, so a new member is a compile error.
Its unreachable default throws rather than falling back to the save wording:
both emitters swallow, so the failure mode is "no toast" rather than a toast
that misstates what just happened to the author's data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-_x4gqw8_.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: render the publish door's advisories — the key #4133 scoped out now exists on PublishMetaItemResponse

2 participants

@os-sam@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); } })(); })();
Skip to content

feat(studio): render the publish door's advisory findings - #6961

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories
Aug 31, 2026
Merged

feat(studio): render the publish door's advisory findings#6961
os-sam merged 2 commits into
mainfrom
claude/issue-5026-publish-door-advisories

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5026

Studio's publish door discarded the runtime authoring gate's advisories. This wires them into the rendering path #4133 / PR #4236 already built for the save door — same component, second source, no new UI shape.

Session for this work: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB

Contract-review conditions, folded in

Both conditions from the ACCEPT-WITH-CONDITIONS verdict are addressed in 1c91abd98.

Condition 1 (blocking) — the changeset is regraded minor and the break is named. The required door member is reader-additive but constructor-breaking: a door-less event literal that type-checked before now fails TS2741. The review measured that on the emitted dist/index.d.ts on both sides, and it is the entire non-comment delta of the package's published surface. My grade of patch was wrong, and the defence I gave for it — "an interface that only this repo constructs" — was an in-repo census standing in for an unmeasurable out-of-repo fact; unmeasurable grades as present, and this PR's own fixture edits are the constructor pattern a consumer's tests would use. The @object-ui/data-objectstack entry now reads minor (every publishable package sits in one fixed group, so that entry carries the group), and the changeset body names the break with its one-line migration: add door: 'save' or door: 'publish', whichever write the literal models. Not major, deliberately — objectui's major is pinned to @objectstack's so that "same major means compatible" holds across the two repos, which is what scripts/check-changeset-no-major.mjs exists to enforce.

Condition 2 — the renderer's door handling is now exhaustive. Covered below, in the very section whose claim it corrects.

The gate measurement came first, and it could have ended the task

This card was held 13 days on a spec-pin condition, released on the observation that the stated reason had expired (the lockfile moved from 17.0.0-rc.2 to 17.2.0). A version number is not a key, so the first action here was reading the installed package, with a hot control.

Measured in this worktree, at runtime, against node_modules/@objectstack/spec — not against framework main, and not inferred from the version:

installed @objectstack/spec version: 17.2.0
--- PublishMetaItemResponseSchema [the card's key] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true
(advisories.0.path | advisories.0.where | advisories.0.message
| advisories.0.hint | advisories.0.severity)
--- SaveMetaItemResponseSchema [HOT CONTROL, carried since #4717] ---
parse WITH advisories -> true
advisories key SURVIVES parse -> true (n=1)
parse WITHOUT advisories -> true | key present: false
reverse probe (partial finding) REJECTED -> true

Three things make that a reading rather than a shrug. Survival, not mere acceptance: an undeclared key is stripped by the object schema, so "parses fine" would prove nothing — the key had to come back out. The reverse probe: a half-shaped finding is rejected, so the key is genuinely validated rather than waved through. And the control: the sibling key on the save door answers identically, so a zero would have meant a broken instrument, not an absent key.

The gate opened. That measurement is now a test (metadata-client.publishAdvisories.test.ts, first describe block) rather than a line in a transcript, so a spec drift fails CI instead of silently re-muting the door.

⚠️ The card's account of the upstream change (objectstack#9176 / PR #9344, 09a6eeee8) is the filing seat's 2026-08-17 report, not my measurement. I did not verify the commit, the PR, or the attribution. What I verified is the state of the installed package, above.

What changed

MetadataClient.publish and MetadataClient.publishDraft — the two methods over the single-item publish route POST /meta/:type/:name/publish — now report through the same sink, the same event and the same renderer the save door already used.

The wiring lands in the data layer, not at the call sites, for the reason PR #4236 gave one door over: every app-shell write path takes its client from useMetadataClient, so one seam covers ResourceEditPage's Publish button (line 1507) and the runtime RuntimeDraftBar promotion behind ObjectView / ReportView / DashboardView, plus any future call site. Both clones (withEnvironment, withPreviewDrafts) already forwarded the sink, and pins cover both.

Why this door is the one that mattered. PR #4236 recorded the gap honestly: Studio's designer stages every edit as a mode: 'draft' save, drafts are never gated (the framework returns at its D1 early-return before a rule runs), and the promotion that is gated declared no advisories field. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's. The second has now expired.

One thing had to differ: the verb

MetadataSaveAdvisoryEvent gains a required door of 'save' | 'publish', and the renderer picks console.publishAdvisoryTitle (added to all ten locale packs) accordingly.

This is not decoration. Save and Publish are two different buttons in this product, so a toast reading "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. And mode cannot answer the question: a direct active save and a draft promotion both report mode: 'publish', because both land the body in the active overlay. A pin asserts exactly that discriminating case.

Required rather than optional-with-a-default so a future third door cannot be wired without saying which one it is; an omitted discriminator would silently render the save wording. Everything else about the surface is unchanged — warning tier, 10s duration, per-finding rule + message + hint, server prose rendered verbatim.

And the renderer handles the union exhaustively (contract-review condition 2). Requiring door guarantees a constructor states a door; on its own it does not guarantee the renderer handles the one it was given. The title choice was a two-way ternary, so a third union member would have compiled at its constructor, declared itself honestly, and still rendered "Saved" — the exact silent-wrong-verb class door exists to kill, reintroduced one level up. It is now a switch with a never check, so a new member is a compile error. Its unreachable default throws rather than falling back to the save wording: both emitters wrap the sink in a try/catch that swallows, so the failure mode is "no toast" rather than a toast that misstates what just happened to the author's data. Two pins cover the runtime half; the compile-time half is tsc's.

⚠️On the clause-② tripwire: carrying advisories required no widening of any ObjectUI-side declared type. It flows through the existing shapes untouched — readSaveAdvisories takes unknown, publishDraft already returned an intersection with Record of string to unknown, and publish is generic in its return. door carries provenance, not advisories, so the tripwire as written was not tripped, and I flagged it rather than burying it.

⚠️ But the review found the letter of that tripwire narrower than its purpose, and it is right: dooris a published-surface delta, and a breaking one for constructors. My accompanying line — "additive on an interface that only this repo constructs" — was an in-repo census doing duty for an unmeasurable out-of-repo fact, and it is withdrawn; see the conditions section at the top for the grade that replaces it. Recorded here because a future dispatch tripwire should read "any change to an exported type", so its letter matches its purpose.

Scope: the batch door is untouched, and that absence is pinned

"Publish whole app" (POST /packages/:id/publish-drafts) still discards per-draft advisories server-side — objectstack#9343 remains open and unruled — and nothing here compensates for that from the client side. That route is reached by a bare fetch in usePublishAllDrafts and by apiJson in PackagesPage; neither goes through MetadataClient, and neither is modified.

A test pins the absence rather than leaving it to a reader's goodwill: a batch-shaped body carrying findings under published[] reaching publishDraft renders nothing. A later "helpful" traversal cannot be added without turning it red.

publishDraft itself is wired because it is the same single-item route as publish, with the same response schema — the orphan-draft fallback in usePublishAllDrafts calls it per item and each response genuinely carries the key. Wiring the door rather than the caller avoids a latent asymmetry inside one route. Nothing traverses a batch response.

Also left alone deliberately: the SDK's meta.publishItem. ObjectStackAdapter's interceptor wraps meta.saveItem only, and publishItem has zero callers in this repo (measured), so wiring it would be surface with no consumer.

Reverse verification

Direction predicted before running: red — removing the two publish-door emits restores parse-and-discard, so every pin asserting an event arrives fails.

Measured at final head 1c91abd98, exactly as predicted — 8 red / 24 green:

× emits the findings a successful promotion returned
× names the PUBLISH door, which `mode` alone cannot say
× carries rule, message and hint through verbatim — they are server prose
× drops half-shaped findings rather than rendering blanks at the author
× survives the withEnvironment clone — console clients are all env-scoped
× survives the withPreviewDrafts clone
× emits the findings a by-reference promotion returned
× reads them through the dispatcher `{ success, data }` envelope it already unwraps
Test Files 1 failed | 1 passed (2)
Tests 8 failed | 24 passed (32)

The mutation was confirmed on disk before the run — anchor count 2 to 0, blob hash 83af5ebd to 43884057 — and the restore was proven the same way rather than by an exit code: restored hash equals the HEAD blob 83af5ebd, git diff HEAD empty, anchors back to 2. Restored run: 32 passed (32).

An earlier revision of this body reported the same eight reds over 22 passed (30), measured at the previous head 3d73b1314. The totals are reconciled rather than merely replaced: this commit adds exactly two tests (the exhaustiveness pair above), and both counts moved by exactly two — the union from 1040 to 1042, and this two-file set from 30 to 32. The red set is identical in every run by either party. The head-accurate figure is the one printed above. No rebuild leg is claimed or required: the subject is reached by a relative source import (from './metadata-client'), so no dist is in the resolution path.

Worth recording, because it says which tests carry the wiring: saveAdvisoryToast.test.ts stayed fully green through the ablation. It exercises the pure builder over a hand-made event, so the publish-door pins in the data layer are what actually guard this — the same asymmetry PR #4236 recorded for the save door. The review took that note further and found the seam neither suite covers (useMetadata.ts lines 130-135, where the hook hands the sink to the factory: cut it and both doors go silent with every named suite green). Inherited from #4236, filed as #6969, not fixed here.

Tests

All at final commit 1c91abd98, union re-run after the last commit:

vitest (root-relative, 65 files) 65 passed, 1042 tests PASS
incl. metadata-client.publishAdvisories, metadata-client.saveAdvisories,
onSaveAdvisory, metadata-client, saveAdvisoryToast,
MetadataService.saveAdvisories, runtime-metadata-persistence,
packages/i18n (locale parity)
type-check data-objectstack / app-shell / i18n all "Done" PASS
check:control-bytes PASS
check:i18n-keys / check:i18n-drift / check:i18n-dead-keys PASS
check:spec-symbols PASS
check:vi-mock-specifiers / check:vi-mock-inherit PASS
changeset:check (check-changeset-fixed + no-major) PASS
check-changeset-overwrite / check-changeset-presence PASS
eslint (touched files, both rounds) 0 errors, warnings only

Every eslint warning is a pre-existing no-explicit-any in a touched file (133 in the first round over 17 files; the second round's two provider files are 0/0); this change adds none.

⚠️One gate is NOT MEASURED locally, and it is not a pass.check:readme-exports exits 1 in this worktree, but every one of its messages is type entry ./dist/index.d.ts is not on disk — run pnpm build first, naming packages the diff never touches. Only the dependency closure was built here, not the whole repo. The gate's own census reports its real verdict classes clean (0 wrong-path, 0 fabricated) and then declares its own population collapse, so the exit code is the unbuilt-tree prerequisite, not a finding. It has its own CI workflow and will be judged there on a fully built tree.

Declared narrowing: the repo-wide pnpm lint scan was not run locally; eslint was run over the touched files instead. CI runs the full farm regardless.

A method note, since it cost a run

The repo's vitest guard (objectui#3378) rejected pnpm --filter PKG exec vitest run …: launched from a package directory, vitest re-roots, the root projects match nothing, and it runs apps/console's 22 files reporting Test Files 22 passed (22) — a green that tested none of the package. Every number above comes from a root-relative invocation. The guard caught it; without it this PR would have shipped a fabricated green.


Generated by Claude Code

The runtime authoring gate reports on BOTH metadata write doors, but objectui
rendered only one of them. objectui#4133 wired the save door and recorded why
that left the common path silent: Studio's designer stages every edit as a
`mode: 'draft'` save, drafts are never gated (the framework returns at its D1
early-return before a rule runs), and the publish step that IS gated declared
no `advisories` field to carry the findings.
`PublishMetaItemResponseSchema` now declares that key (objectstack#9176), so
`MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods
over `POST /meta/:type/:name/publish` — report through the same sink, event and
renderer the save door already used. The wiring lives in the data layer, so the
ResourceEditPage Publish button and the RuntimeDraftBar promotion are covered
by one change rather than a toast per call site.
`MetadataSaveAdvisoryEvent` gains a required `door: 'save' | 'publish'`: Save
and Publish are two different buttons here, so "Saved" after a Publish would
tell the author their change is still a draft. `mode` cannot answer this — a
direct active save and a draft promotion both report `mode: 'publish'`.
The batch door (`POST /packages/:id/publish-drafts`) still discards per-draft
advisories server-side and nothing here compensates for it; a test pins that
absence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.1 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-C5WkKps1.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

… exhaustively
Two conditions from the contract review on this branch.
Condition 1 (blocking). The required `door` member on the exported
`MetadataSaveAdvisoryEvent` is reader-additive but constructor-breaking: a
door-less event literal that type-checked before now fails with TS2741. It was
graded `patch`. Regraded to `minor` on the data-objectstack entry, which carries
the fixed group, with the break and its one-line migration named in the
changeset body. Never `major`: objectui's major is pinned to `@objectstack`'s so
that "same major means compatible" holds across the two repos, so objectui's own
breaking changes ship as `minor` with the break spelled out
(scripts/check-changeset-no-major.mjs).
Condition 2. `door` being required guarantees a constructor STATES a door; it
did not guarantee the renderer HANDLES the one it was given, because the title
choice was a two-way ternary. A third union member would have compiled at its
constructor, declared itself honestly, and still rendered "Saved" — the exact
silent-wrong-verb class `door` exists to kill, one level up. The choice is now
an exhaustive switch with a `never` check, so a new member is a compile error.
Its unreachable default throws rather than falling back to the save wording:
both emitters swallow, so the failure mode is "no toast" rather than a toast
that misstates what just happened to the author's data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-_x4gqw8_.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)12.46KB4.71KB
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)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.65KB61.63KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: render the publish door's advisories — the key #4133 scoped out now exists on PublishMetaItemResponse

2 participants

@os-sam@claude