Repository navigation
feat(app-shell): the suggested-bindings panel shows each suggestion's state and revokes an acceptance (part of objectui#7611) - #12148
Conversation
…and revokes an acceptance After objectstack#22854 (#22801 -> A) an accept by a manage_metadata holder takes effect for the whole deployment and a dismiss on an accepted row revokes it. The panel listed pending rows only and dropped bindingCreated. It now lists every row of the package and draws pending (Accept / Dismiss), a guest row (no Accept, the reason instead, Dismiss), accepted (Revoke, through the dismiss call), accepted but not yet in force (a 200 answered bindingCreated: false) and dismissed or revoked. A baseline row is not drawn. A 403 or 409 on an action shows the server's message and re-reads the list; a 403 on the list still renders nothing. Part of objectui#7611. Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR Co-authored-by: Claude <noreply@anthropic.com>
…ell, i18n minor) Also types the panel's catch as unknown and keys the not-in-force set on the accept response's id. Part of objectui#7611. Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR Co-authored-by: Claude <noreply@anthropic.com>
…pans The five new de values quote the set and the position with matched „…“ pairs, seven spans between them; the census moves 77 -> 84 with rdq 0. Part of objectui#7611. Claude-Session: https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR Co-authored-by: Claude <noreply@anthropic.com>
✅ 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
|
Contract reviewServed-tier: Inputs: card #7611 (body and all 25 comments — the part-1 records, the scoping follow-up 6110524912, the claim amendment 6110601135 and the dev report 6111080375 among them), PR #12148 (body, 18-file list, net diff against ① Derived judgmentsEach line names a change the diff implies, then judges it against the server at
② Semver level
③ Boundary flagsDev deviations (4, report 6111080375) — each answered:
Open questions (2) — each answered on the record:
Out-of-scope findings (2, carrier none) — accepted as filed; they are the two questions above, and the first is escalated. Reviewer's flags (3) — none fails the head:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #7611
Clause-②: yes (widening)
Draft by dispatch (objectui AGENTS.md §9 ②). This is C9 part 2: the
SuggestedBindingsPanelfollow-up recorded in 6110524912 and claimed by the amendment 6110601135, after objectstack#22854 (stage U2,d112087b8980). The epic PM of objectstack#15194 moves this PR. The rest of #7611 stays open; part 1 landed as0df67f237c5a.What objectui now shows
On a framework at objectstack
maind112087b8980or later, amanage_metadataholder's accept takes effect for the whole deployment, and a dismiss on an accepted row revokes it (objectstack#22801 → A). The panel on the marketplace package page and in the Studio Access pillar used to listpendingrows only and droppedbindingCreated. It now lists every suggestion of the package (GET /api/v1/security/suggested-bindings?packageId=…, no status filter) and draws each in the state the server records:status: pending, anchoreveryonestatus: pending, anchorgueststatus: confirmed,deployment_decision: true200withbindingCreated: falsestatus: dismissedstatus: confirmed,deployment_decision: false(the baseline names the set)POST …/:id/dismiss) on an accepted row. The success toast says the acceptance was revoked.bindingCreatedistrue. Onfalseit shows the not-in-force line as a warning toast, and the row keeps Revoke, because the acceptance is recorded.confirmSuggestedBindingnow returns{ suggestion, bindingCreated }instead of dropping the flag.403or409on Accept, Dismiss or Revoke shows the server's ownerror.message, never a silent no-op. The panel then reads the list again, so the row shows what the server holds.403on the list (a caller withoutmanage_metadata) still renders nothing. The list's.catchin the panel's mount effect still swallows the refusal.everyonecarries an accepted set, so "not yet in force" lasts until the panel re-reads. See the acceptance notes.Why a
guestrow shows the reason instead of rendering nothingThe server refuses a
guestaccept with409, and still accepts a dismiss of a pendingguestrow. The panel therefore draws no Accept, says why, and keeps Dismiss. This follows the convention part 1 of this card set (0df67f237c5a): a caller the server would refuse gets no affordance, and the page says why. It also follows objectui#12109's rule that an affordance the server refuses is not drawn. Rendering nothing would leave a pending row that no surface can show or dismiss. No producer emits aguestsuggestion today, so this branch is dormant until one does.Why a baseline row is not drawn
Its dismiss answers
409("this surface does not decide the baseline"), and its confirm answers409too. No action on it can succeed, and nothing is pending. Before this change the panel did not draw it either.Measured against objectstack
mainThe measurement booted a showcase from a detached objectstack worktree at
55382dc02a(itsorigin/main, which containsd112087b8980). It ran through a throwaway, untracked dogfood-style file, deleted after the run, sogit statusin that worktree is clean. The driver wassqlite-wasm. Readings, in order:SecurityPlugin(soshowcase_member_defaultis a package suggestion), the list row ispending, withdeployment_decision: false,resolved_by: nullandanchor: "everyone".200,{ data: { suggestion, bindingCreated: true } }. The suggestion isconfirmedwithdeployment_decision: trueandresolved_byset.status: "confirmed",deployment_decision: true(a JSON boolean) andresolved_byset. It has no field that says whether the set is in force.409 SUGGESTION_STATE: "… is already confirmed — only pending suggestions can be accepted."200, and the suggestion isdismissedwithdeployment_decision: true. The list then shows the same. A revoked row and a dismissed pending row are identical on the wire.409: "… is already dismissed — only a pending or accepted suggestion can be dismissed." An accept on that row answers409("… only pending suggestions can be accepted").403 PERMISSION_DENIED: "[Security] Access denied: listing audience-binding suggestions requires themanage_metadatacapability. …"confirmed,deployment_decision: false,resolved_by: null. Its dismiss answers409: "… is observed, not accepted: the deployment's baseline gives the set to everyone, and this surface does not decide the baseline."bindingCreated: falseon a200could not be produced through the public door on that boot: the confirm and the derivation ask the same gates. It is NOT MEASURED live here. objectstack pins it in its unit suite, and #22854's ablation showed the flag turningfalse. The panel's handling is pinned below against the documented envelope.Tests
The new
packages/app-shell/src/components/SuggestedBindingsPanel.test.tsxhas 15 cases. It renders the real panel and the realsuggestedBindingsApiclient over a stubbedfetchthat answers the server's envelopes, with row shapes and messages taken from the measurement above. There was no test of this component before. The file follows app-shell's convention for a component with a stubbed transport:vi.stubGlobal('fetch', …),sonnermocked, and@object-ui/componentsimported at module scope.guest(no Accept, the reason shown, Dismiss kept). One test pins that a403on the list renders nothing, and one that the list asks for every status.bindingCreated: true; Accept withbindingCreated: false(not in force, a warning and no success toast); Dismiss; Revoke (the dismiss route, never confirm); Revoke refused409(the message is shown, the list is re-read, and the row takes the server's state); Accept refused403; and Revoke refused403followed by a list403(the message is shown, then the panel renders nothing).Ablation (one-time, not kept). Each mutation went through objectstack's
scripts/ablation-replace.mjs, which checks that the anchor hit and that the restore holds. Each restore read blob == HEAD withgit diff HEADempty.bindingCreated: data?.bindingCreated === true→bindingCreated: true: exactly the not-in-force test went red (1 failed, 14 passed).return s.anchor === 'guest' ? 'guest' : 'pending';→return 'pending';: exactly theguesttest went red (1 failed, 14 passed).s.status === 'confirmed' && s.deployment_decision === true→s.status === 'confirmed': exactly the two baseline tests went red (2 failed, 13 passed).At head
8d3b3a0eb(all runs under the shared verify lock):pnpm exec vitest runover the new file,packages/app-shell/src/console/marketplace/(its 29 test files mock the panel), the three Studio Access-pillar suites,studio-locale.i18n,select-placeholder-literal-11252, all ofpackages/i18n/, and the three i18n script tests: 120 files passed, 1812 tests passed, 13 skipped. The skips are pre-existing and none is in the new file.pnpm --filter @object-ui/app-shell --filter @object-ui/i18n run type-check, which istsc --noEmitplustsc -p tsconfig.test.json: bothDone.--listFileson app-shell's test project lists the new test file once.packages/i18n/src/__tests__/de-quote-pairing-3876.test.tswent red on the first run. It pins the de census, and the five new de values add seven matched„…“spans. The census is moved 77 → 84 withrdq0, and a history line is added like those before it.Narrowed, and declared. The whole of
packages/app-shell/(1,271 test files) was started at--maxWorkers=2. After 19 minutes it had not finished, and a sibling's build was queued behind it. I stopped it before it printed a result, so that run is NOT MEASURED. CI'spnpm testruns the whole package. The diff changes no export and no shared module outside the panel, its client, the two mounts' string objects and the locale tables.Gates
At
8d3b3a0eb, each exit 0:check-i18n-call-site-keys,check-i18n-en-drift,check-i18n-designer-table-parity,check-control-bytes,check-new-cross-file-line-citations(0 new),check-changeset-presence(17 source files of 2 released packages, 1 changeset),check-changeset-no-major,check-vi-mock-override-shape,check-test-path-roots.check-i18n-dead-keysis report-only and names none of the new keys. A control-byte self-scan over the diff found nothing.ESLint (
--no-inline-config,--format json) over the 17 touched TS files reports 1 error and 29 warnings. The base file under the same flags reads the same:StudioDesignSurface.tsx1 error and 14 warnings,MarketplacePackagePage.tsx0 and 11,suggestedBindingsApi.ts0 and 4. The error is the pre-existingreact-hooks/static-componentsfinding in that file, far from this diff. The panel goes from 1 warning to 0. The population is each package's owneslint .under the rooteslint.config.js. That config enables no type-aware linting, so this diff cannot change a verdict on an untouched file. The repo-widepnpm lintis CI's.NOT MEASURED locally:
check-eager-closure-budgetandcheck-eager-locale-catalogues, which need a full console build and are CI's (performance-budget.yml). Theenpack grows by seven short values. A browser drive of the panel is also not measured: a stock showcase has no pending suggestion for the panel to draw, and a vanilla-plugin boot needs an objectstack configuration change this card does not make. The wire was measured instead, above.Changeset
.changeset/7611-suggested-binding-states.md:@object-ui/app-shellminor (a new user-visible action) and@object-ui/i18nminor. Clause-②: yes (widening): seven locale keys are added to the published packs (marketplace.suggestedBindings.guestUnsupported,acceptedState,notInForceState,dismissedState,revoke,revoking,revokedToast), which widensTranslationKeys. No export, prop or accept set moves. The panel and its client are not exported from@object-ui/app-shell. The Studio Access pillar's own table gains the matching sevenengine.studio.access.suggest…keys in English and Chinese.No doc names this panel: a grep of
content/and every*.mdoutside changesets and changelogs finds nothing, so no doc is updated.Acceptance notes (out of scope; carrier: none named)
everyonecarries the set or the derivation withholds it. A withhold happens when a later publish adds a forbidden bit, or the catalog stops serving the set. So the panel's accepted line claims the decision only, and "not yet in force" survives only until the panel re-reads. Closing that would take a server field on the list row. That is an objectstack contract question, not an objectui fix.dismissedwith a deployment decision. The reconcile never re-opens such a row, and confirm takespendingonly. Re-accepting a revoked set therefore has no route here, which the measurement above shows (409on confirm after a revoke). Dismissing a pending row was already final before #22854.Size
Against base
158c75d(git diff --shortstat 158c75d 8d3b3a0eb): 18 files, 667 additions and 79 deletions, so 746 changed lines. The new test file is 288 of them, and the ten locale packs are 70.Generated by Claude Code