Uh oh!
There was an error while loading. Please reload this page.
feat(studio): render the publish door's advisory findings - #6961
Merged
Conversation
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
Contributor
✅ Console Performance Budget
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
Size Limits
|
This was referenced Aug 31, 2026
… 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
Contributor
✅ Console Performance Budget
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
Size Limits
|
os-sam
marked this pull request as ready for review
August 31, 2026 05:10
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Sep 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
minorand the break is named. The requireddoormember is reader-additive but constructor-breaking: a door-less event literal that type-checked before now failsTS2741. The review measured that on the emitteddist/index.d.tson both sides, and it is the entire non-comment delta of the package's published surface. My grade ofpatchwas 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-objectstackentry now readsminor(every publishable package sits in onefixedgroup, so that entry carries the group), and the changeset body names the break with its one-line migration: adddoor: 'save'ordoor: 'publish', whichever write the literal models. Notmajor, deliberately — objectui's major is pinned to@objectstack's so that "same major means compatible" holds across the two repos, which is whatscripts/check-changeset-no-major.mjsexists 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.2to 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 frameworkmain, and not inferred from the version: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.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.publishandMetadataClient.publishDraft— the two methods over the single-item publish routePOST /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 coversResourceEditPage's Publish button (line 1507) and the runtimeRuntimeDraftBarpromotion 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 noadvisoriesfield. 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
MetadataSaveAdvisoryEventgains a requireddoorof'save' | 'publish', and the renderer picksconsole.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
modecannot answer the question: a direct active save and a draft promotion both reportmode: '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
doorguarantees 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 classdoorexists to kill, reintroduced one level up. It is now aswitchwith anevercheck, so a new member is a compile error. Its unreachabledefaultthrows 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 istsc's.advisoriesrequired no widening of any ObjectUI-side declared type. It flows through the existing shapes untouched —readSaveAdvisoriestakesunknown,publishDraftalready returned an intersection withRecordof string to unknown, andpublishis generic in its return.doorcarries provenance, not advisories, so the tripwire as written was not tripped, and I flagged it rather than burying it.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 barefetchinusePublishAllDraftsand byapiJsoninPackagesPage; neither goes throughMetadataClient, 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[]reachingpublishDraftrenders nothing. A later "helpful" traversal cannot be added without turning it red.publishDraftitself is wired because it is the same single-item route aspublish, with the same response schema — the orphan-draft fallback inusePublishAllDraftscalls 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 wrapsmeta.saveItemonly, andpublishItemhas 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:The mutation was confirmed on disk before the run — anchor count 2 to 0, blob hash
83af5ebdto43884057— and the restore was proven the same way rather than by an exit code: restored hash equals the HEAD blob83af5ebd,git diff HEADempty, 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 head3d73b1314. 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 nodistis in the resolution path.Worth recording, because it says which tests carry the wiring:
saveAdvisoryToast.test.tsstayed 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.tslines 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:Every eslint warning is a pre-existing
no-explicit-anyin 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.check:readme-exportsexits 1 in this worktree, but every one of its messages istype 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 lintscan 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 runsapps/console's 22 files reportingTest 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