feat(data-objectstack,app-shell): route "publish whole app" through the advisory reporting seam (objectui#6965) - #10038
Conversation
…he reporting seam (objectui#6965) The batch publish door answers the runtime authoring gate's per-draft advisories on each `published[]` element (objectstack#9343), and the author saw none of them: both client call sites fired the route outside the seam that reports — a bare `fetch` in `usePublishAllDrafts`, and `apiJson` in `PackagesPage` under a declared response type with no `published[]` at all. - `MetadataClient.publishPackageDrafts` expresses the route and emits one advisory event per advised element, each naming that element's own type / name, into the sink and renderer both other write doors use. - `door: 'publish'` is reused rather than extended: every item the event names really was published, and the verb is the renderer's only door-dependent output. - Only what the server sent, where the published schema declares it — a half-shaped finding, an element that cannot name its item and a top-level `advisories` all report nothing. - The batch absence pin is flipped to a presence pin, keeping the invent-nothing half it was really carrying, and the two sentences that said the batch route still discards advisories are retired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
… in the pending objectui#5026 declaration Prose only, frontmatter byte-identical (the overwrite gate prints both, and they match). The paragraph asserted, in the present tense, that the batch publish door "still discards per-draft advisories server-side" and that "a test pins the absence". Both went false: objectstack#9343 landed, and this branch flips that pin to a presence pin. A pending changeset publishes VERBATIM into the CHANGELOG at the next release, which is the last moment this is correctable — afterwards it is historical record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
|
changeset-claim-re-read
|
✅ 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
|
…sories objectui#8637's PR #10034 landed while this branch was reporting, so the base moved. Merge rather than rebase: this branch is pushed and may be checked out elsewhere, and the repo's convention is that history on a pushed branch is never rewritten. The squash merge drops this commit from main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
…ll tuple has an element 0 `tsc --noEmit` (the package's own `type-check`, which covers `src/**`) refused the new route pin with TS2493, "Tuple type '[]' of length '0' has no element at index '0'": the spy was `vi.fn(async () => …)`, declaring no parameters, so vitest inferred the call tuple as `[]` and the non-null assertion silenced the outer index rather than the inner one. Typed like `fetch`, which is the spelling `exportDownload.test.ts` in this same package already uses and documents for exactly this reason. `_url: string` rather than `RequestInfo | URL` because this client builds its URL as a string and the assertion is meant to keep checking that: with the parameters declared, the destructured element is typed `string`, so the pin still fails on a wrong URL and still refuses a non-string one — neither of which an `as any` on the assertion would have preserved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
✅ 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
|
Fixes #6965
Studio's "publish whole app" showed the author none of the runtime authoring gate's findings, although the server had been sending them since objectstack#9343 landed. Both client call sites fired
POST /packages/:id/publish-draftsoutside the seam that can report — a barefetchinusePublishAllDrafts, and the page-privateapiJsoninPackagesPageunder a declared response type holding two counts andfailed[], with nopublished[]at all. The same button's own client-side capability lint was raising a toast the whole time, so the one thing that could not reach the person pressing it was a finding from the server.Readings below were taken in a worktree off
origin/main030a675b0, 2026-09-19T18:30Z onwards.The premise, re-verified rather than inherited
@objectstack/specINSTALLED in this worktree: 17.4.0 (node_modules/@objectstack/spec/package.json).PublishPackageDraftsResponseSchemain its own source declarespublished[].advisoriesas an optional array ofRuntimeAuthoringIssueSchema, "riding each element rather than a parallel top-level map" — the ruled shape, in the schema's own prose.check:spec-floorsresolved the DECLARED floor@objectstack/spec@17.2.0from the registry and judged data-objectstack's built artifact against it with no finding, so the symbol this package now imports is present at the floor it declares, not only at the version installed here.1. The seam
MetadataClient.publishPackageDrafts(packageId)expresses the route and emits oneMetadataSaveAdvisoryEventper advisedpublished[]element, each naming that element's owntype/name, through the same sink, event and renderer the save and single-item publish doors use. Both call sites go through it.Three decisions worth reviewing, each stated in the code where it is made:
PublishPackageDraftsResponseSchemasays it describes the full body "inside the dispatcher's{ success, data }envelope", so the declared object is the inner one.publishDraftstill refuses to unwrap, because its schema describes the full body of a route the REST server answers verbatim (objectui#6962). Same rule, read per route.MetadataErroras every other method does; a 2xx is returned unexamined, becausesuccess: falseis not a refusal on this route (outcome: 'nothing_to_publish'answers it too) and the two callers judge it differently today.PackagesPagekeeps the objectui#7959 ADR-0112 ladder — a producer-markederror.userMessageoutranks the diagnostic — by re-reading it off the raised error's captured body./environments/:id/packagesmirror is a route nothing in this repo has shown exists, and scoping to it would trade a working call for a 404. Pinned.2. Why
door: 'publish'— and why the fall-back clause did NOT fireThe dispatch bounded me: a design needing a third value in the published
doorunion could not be built here, and the correct move in that case was to stop and report. It did not come to that, and here is the reading rather than an assurance.doorhas exactly one consumer:advisoryTitleinproviders/saveAdvisoryToast.ts, whose only door-dependent output is the frame's verb — "Saved" for'save', "Published" for'publish'. Nothing else in the renderer branches on it. Every item a batch event names really was promoted to active, so "Published" is the true verb for it; a third value would have to render the same word. The per-item identity the author needs in order to go and fix something is carried bytype/name, one event per advised item, so nothing about the batch is lost by sharing the value.What that argument does NOT say, stated in the type's own docblock so the next reader cannot take it for more: it is not a claim that one call produces one event. The batch emits as many events as it has advised items, which is exactly why each one names its own.
The exhaustive
switchwith thenevercheck is untouched, so a third door remains a compile error rather than a silently wrong verb.3. The flipped pin — RED first, then GREEN
The absence pin
does NOT render advisories buried in a batch-shaped published[] bodyis flipped, not deleted, and what it was really carrying — the client renders only what the server sent — is pinned in both directions.Reading A — the OLD assertion against the NEW code. The flipped case's assertion was replaced by the old one,
expect(events).toEqual([]), on the committed tree. Anchor occurrences before the edit: 1; after: 0, with the injected text present on disk (an anchored replace that refuses a no-op, rather thansed -i, which exits 0 on zero matches).So the flip is a real behaviour change and not a rewording. Restored with
git checkout HEAD --;git diff HEADempty and the worktree blob equal toHEAD's (6bfaa8f94d88cf18c4a3719ad0a2e9e8d5143362).Reading B — the implementation ablated. The per-element emit loop was removed from
publishPackageDrafts(anchor 1 before, 0 after, injected marker on disk; the file's other twoemitAdvisoriescall sites left standing):Restored the same way, blob equal to
HEAD's (1d5fa99c3b73d276e3af10ea2d053ae2e7e192b4).⭐ Worth naming: two of the three "INVENTS NOTHING" cases stayed GREEN under ablation, because a pin that asserts an absence cannot fail when the emitter is gone. That is the honest reason the presence cases have to exist beside them, and why one green would have proved nothing here.
Reading C — green on the restored tree.
Tests 30 passed (30).4. The two falsified sentences, retired — and a third the card did not know about
metadata-client.ts,publishDraft's docblock: the paragraph asserting the batch route "discards per-draft advisories server-side; that is objectstack#9343 and nothing here compensates for it" now records that the card landed, and keeps the half that is still true and still about THIS method — it reads the top level of the single-item body and nothing else, so a batch-shaped body arriving there reports nothing. That is pinned.metadata-client.publishAdvisories.test.ts, the module docblock's scope-control section: rewritten around what the file now pins, in both directions..changeset/render-publish-advisory-findings-5026.mdcarried the same claim in the present tense — "still discards ... A test pins the absence" — and a pending changeset publishes VERBATIM into the CHANGELOG at the next release, which makes this the last moment it is correctable. Corrected in its own commit, prose only;check-changeset-overwrite.mjsreports it as its case 2 ("correcting a declaration on purpose") and prints the frontmatter on both sides, unchanged.5. Scope
Declared file face, all four touched:
metadata-client.ts,metadata-client.publishAdvisories.test.ts,usePublishAllDrafts.ts,PackagesPage.tsx. Two files beyond it, both declared here: the new changeset this change owes, and the pending changeset correction above.MetadataPublishPackageDraftsResultis deliberately NOT added to the package barrel: the published surface grows by the one method deliverable 1 requires and nothing else.Validation
pnpm exec vitest run packages/data-objectstack/— 67 files, 926 passed.pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ packages/app-shell/src/preview/— 298 files, 3160 passed, 1 skipped.pnpm --filter @object-ui/data-objectstack run type-check—29768c3e6):TS2493on the new pin'sfetchspy, caught by CI (Type Check, job 105950929212) and then reproduced locally byte-for-byte. GREEN on723dd32b1, exit 0. The exit-0 this line carried before was a real reading of a different tree — it was taken right after the dependency-closure build, when the failing test case did not exist yet, and nothing run afterwards could see it (vitest type-checks nothing, and app-shell's type-check reads this package's built.d.ts, never itssrc). The general form, stated because it is the reusable part: a measurement must follow the last edit to what it measures.pnpm --filter @object-ui/app-shell run type-check— exit 0 (tsc --noEmitand the test tsconfig), after building the dependency closure under the shared verify lock (pnpm --filter '@object-ui/app-shell^...' build, exit 0). Re-run on723dd32b1after the base merge.domain:ui#2PM seat (sessionsession_018HrVaotisyhgmot9o2MLRq) at the dev's request, recorded on objectui#6965. The dev writes this body once at creation and does not edit it; ⛔ nothing else here was changed.pnpm exec eslint --no-inline-configon the four changed source files — exit 0, 0 errors. 29 warnings, and the attribution is measured rather than asserted: intersecting the JSON report's line numbers with this branch's added lines gives 0 findings on a line this branch added, 29 on untouched lines. Control for that zero: the same intersection counts 21 / 66 / 314 / 212 added lines in the four files, so the instrument was looking at real additions.check:control-bytes,check:spec-symbols,check:installed-pin-claims,check:new-line-citations(0 new),check:test-path-roots,check:vi-mock-specifiers,check:i18n-keys,check:self-import,check:changeset-presence,check:changeset-no-major,check:changeset-fixed,check:pending-changeset-literals— all exit 0.check-governed-queue-guard.mjs --teston all six paths: NOT GOVERNED.check:spec-floorsis NOT MEASURED as a whole: it exits 1 on an unbuilt tree with fourno-artifactfindings (app-shell, plugin-gantt, plugin-timeline, plugin-tree — "produced no build output to judge"), which needs a full workspace build. The package whose spec-symbol usage this change moves, data-objectstack, WAS inspected in that run and produced no finding.pnpm lintand the full suite are CI's runs. The eslint narrowing above is a measurement rather than a skip: the population is eslint's own flat config, the file count is read off--format json, and no type-aware linting is configured ineslint.config.js(noparserOptions.project, noprojectService), so this diff cannot move the verdict on a file it does not touch.Acceptance notes
StudioDesignSurface.tsx,console/ai/PendingDraftsBar.tsxandconsole/ai/AiChatPage.tsxeach fire the same route with their ownfetch, so an author publishing from Studio's design surface or the chat bar still sees no advisories. Same defect, same fix shape, outside the declared file face and outside a dispatch that said not to do more. Filed as a finding in the report, with dedupe words: batch publish-drafts bare fetch, StudioDesignSurface, PendingDraftsBar, AiChatPage, advisory seam.Changeset Claim Re-readrequest, answered. That gate flags.changeset/publish-envelope-one-spelling-6962.mdbecause it namesmetadata-client.ts. Its paragraph is still TRUE: this change does not touch either single-item method's envelope behaviour, and the new method's unwrap is the other route's declared shape, stated beside it.publishDraft's older sentence aboutlistDraftstolerances and this file's other per-route notes are unaffected; no successor is needed.Session reference, in prose because a footer does not reliably survive an edit:
https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq🤖 Generated with Claude Code
https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Generated by Claude Code
Generated by Claude Code