feat(data): surface save advisories from the second metadata client class (#4237) - #4258
Merged
Merged
Conversation
…lass (#4237) `ObjectStackClient.meta.saveItem` reaches the same `PUT /api/v1/meta/:type/:name` door as `MetadataClient.save`, but #4133/#4236 only covered the latter — every caller of the former awaited the call and discarded the response, dropping any `advisories[]` the runtime authoring gate attached to a successful save. `ObjectStackAdapter` gains an `onSaveAdvisory` subscription, a sibling of the existing `onWriteWarning` seam (#3431/#3455), emitted from ONE interceptor installed on the adapter's own long-lived client. Every enumerated caller (MetadataService, useNavigationSync, plugin-designer's app wizard, and the adapter's own view/dashboard save paths) reaches the door through that client, so all of them are covered without a per-site edit. `AdapterProvider` subscribes once and renders through the same `emitSaveAdvisories` the other client class uses; `readSaveAdvisories` is shared unchanged — one reader, two call sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
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 free
to 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 #4237
objectui#4133 (PR #4236) put the runtime authoring gate's advisory findings in front of Studio authors — the findings that ride a 200, where the save succeeded, the row persisted and the version bumped, and the gate merely noticed something worth saying. That wiring lifts once at
useMetadataClient, which is where every app-shell path takes itsMetadataClientfrom.ObjectStackClient.meta.saveItem— the SDK client hanging offObjectStackAdapter— is a different class reaching the same door, and it was not covered. Every one of its callers awaited the call and discarded the response, so anadvisories[]the server attached was parsed off the wire and dropped one layer further out. All of them write in active mode, so this is not the draft case where the gate never runs: the gate does run for them, produces findings, and the author was told nothing.The seam, and which responses flow where
One emitter, installed on the adapter's own long-lived client the moment it is constructed. Every caller the card enumerates reaches the save door through that client — the four inside
data-objectstackviathis.client, everything outside it viagetClient(), which hands back the same instance — so one interception covers all of them with no per-site edit.Why a sibling seam and not the
onWriteWarningevent itselfThe card asks for the emitter shape of
onWriteWarning(#3431/#3455) rather than #4236's config-callback shape, and that is what this does — a long-lived instance with asubscribe → unsubscriberegistration, wired once inAdapterProviderright next to the existing one. It reuses that seam's shape, not its event type, becauseWriteWarningEventis a closed shape whose requireddroppedFieldsmeans "fields the write legally stripped". Carrying advisories on it would either force every existingonWriteWarningsubscriber to grow a branch — breaking the "existing consumers unchanged" control — or make the event lie about what happened.MetadataSaveAdvisoryEvent's own declaration already said it was "deliberately the same shape of seam asObjectStackAdapter.onWriteWarning", so this is that sentence carried out. A test pins that a metadata save does not leak onto the write-warning channel.emitSaveAdvisoriesandreadSaveAdvisoriesare reused unchanged — one reader, two call sites.Response shape — measured, not assumed
The premise check that mattered was whether
readSaveAdvisoriescan even read this client's response, since the two clients could have differed in envelope. They do not, and the reason is specific:SaveMetaItemResponseSchemadeclaresadvisoriesat the body's top level, next tosuccess/version/seq/state, with exactly the six keys the shared reader requires.unwrapResponsestrips its{ success, data }envelope only when the body has adatakey. The save body does not, so it comes back verbatim.Both halves are pinned rather than trusted: the tests drive a real SDK client through a fake
fetchinstead of stubbingmeta, and one case asserts the resolved value still carriesversion/seq/advisories. If a future SDK started wrapping this response, that case goes red and says so, instead of the channel quietly emitting nothing.Draft-door honesty (D1)
Drafts are never gated — the framework returns at its D1 early return (
if (args.state !== 'active') return null) before running a rule, so a draft save produces no findings to withhold. This client class has no draft door at all: the SDK'ssaveItem(type, name, item)takes no mode and always writes the active door, which is exactly why the gate runs for its callers.modeon the emitted event is therefore derived from the response's ownstaterather than from a request-side flag that does not exist here, so the event stays truthful about which door it came through instead of hard-coding one. Both halves are pinned: a draft-state response with no findings emits nothing, and a draft-state response that does advise is labelleddraft.Evidence
Reverse verification — the emitter removed, subscriptions left intact (so the failure mode is "silently discarded again", not a TypeError). Predicted before this change: red; after: green, with the negative controls staying green either way. Measured exactly that, 12 red / 7 green:
The 7 that stayed green are the honest ones: the four negative controls (clean save, empty array, no leak onto the write-warning channel, draft-state emits nothing), the SDK-envelope measurement, and the two "the save still happened" pins. None of them can observe the emitter, so none of them should have moved.
Gates (repo root, affected packages):
Build closure (
pnpm --filter '@object-ui/app-shell^...' build) ran before type-check, so nothing was judged against a stale.d.ts.Scope
The enumerated call sites are deliberately not edited — that they need no edit is the claim under test, and
MetadataServiceis pinned end-to-end for exactly that reason.packages/i18nis untouched: the wording keyconsole.saveAdvisoryTitlealready ships in all ten packs from #4236, and both doors now render one wording.Generated by Claude Code