Repository navigation
feat(components): the four action:* blocks publish the spec keys their block path honours (objectui#11168 slice 1) - #11185
Conversation
…their renderers read (objectui#11168) Slice 1 of objectui#11168, the `action:group` / `action:menu` half. Each key was measured through the real SchemaRenderer against the installed @objectstack/spec 17.5.0 rows (objectui#11111 decision 3 = B): - declared: `action:group.location`, `action:group.visible`, `action:menu.size`, `action:menu.visible`; - retired: `action:group.name` (read by nothing, refused by the spec); - narrowed: `actions` on both blocks is a list of action objects, and `action:group.size` publishes the Button primitive's four sizes without the refused `md`; - the member shape of both `actions` lists is pinned. The guard's booked entries for these keys are struck and the caps, the member-pin ceiling and the owner count follow them down. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
… the block path honours (objectui#11168) Slice 1 of objectui#11168, the `action:button` / `action:icon` half. Every spec key the 17.5.0 rows declare and the registrations did not publish was driven through the real SchemaRenderer, the renderer's forward and the real ActionRunner (objectui#11111 decision 3 = B): - declared on both: `visible`, `disabled`, `params`, `description`, `openIn`, `method`, `bodyExtra`, `bodyShape`, `operation`, `patch`, `confirmText`, `successMessage`, `errorMessage`, `refreshAfter`, `locations`, `toast`, `resultDialog`, `onSuccess`, `objectName`; `recordIdField` on `action:button` too; - held unpublished, still booked to the card with the measurement: `endpoint` on both (the console's own `api` handler never reads it) and `undoable` on `action:button` (Undo needs a host row stash the block never writes). Each declared key is pinned on the effect its description promises, with a control row, and the ten array/object-armed keys per block get member pins. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…dger for the published action keys (objectui#11168) - The changeset names every key slice 1 declared, retired or narrowed on the four `action:*` blocks, the break the narrowed arms state, and the keys still held unpublished. - The layout guide says which action keys are authored on the node, and that an `api` action's URL goes in `target`. - `public-block-binding-reach` derives its population from blocks that publish `objectName`; the two action leaves now do, and on them the key names the object an action is dispatched against rather than a data binding, so both are listed and ledgered in `NO_DATA_REACH` with that reason (asserted: the block still fetches nothing under the name). Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…he action-leaf pin (objectui#11168) The `refreshAfter` row read the provider's result by reassigning a module variable inside the probe component, which `react-hooks/globals` refuses. The probe now reports the result through an `onResult` prop from an effect, and the row reads it through a getter so the reset between mounts does not narrow its type. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
|
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
|
Contract reviewServed-tier: Director seat (objectstack#12708, summon #30 续 2), on the standing ask 「契约复审」 and the PR's own line "the PR waits as a draft for the director seat's contract review". Inputs: card objectui#11168 (the slice plan and the objectui#11111 decision-3 = B ruling 5902351047 it cites), PR #11185 (body with its per-key verdict tables, the ten-file list, the net diff against ① Derived judgmentsAccept-set change: a widening and a narrowing of the published
② Semver levelThe body's ③ Boundary flags
Check-runs on the head, read 2026-09-30T09:33Z — 43 runs, none Implemented-by: VERDICT: PASS |
…jectui#11168) PR #11184 (objectui#8649) landed as 6479086 and struck its nine field-security rows from the objectui#11111 ledger; slice 1 of objectui#11168 struck its own. The one conflict, in apps/console/src/__tests__/registry-inputs-spec-parity.test.ts, is resolved by re-deriving every figure from the merged ledger, counting its entries per ledger and per owner: - OBJECTUI_11111_LEDGER_CAPS: unjudgedBlocks 4, offSpecInputs 0, unpublishedKeys 5, refusedArms 2, memberPins 4. - Owner-count pin: objectui#11168 13, objectui#8652 2, objectui#8649 0. objectui#8649 stays in OBJECTUI_11111_OWNERS at 0, the convention its own landing used for an owner with nothing left. - The UNPUBLISHED_EXEMPTIONS booking docblock takes main's wording as landed. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm 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: Isolated contract reviewer for the ① Derived judgmentsThe published
Nothing else in the ten files changes an accept set or a public surface. ② Semver levelThe diff publishes a widening — 43 keys the manifest, the JSX intrinsics and the page validator now accept (20 + 19 + 2 + 2) — and a narrowing: ③ Boundary flags
Check-runs on Implemented-by: VERDICT: PASS Generated by Claude Code |
Dequeued by a required check that is red on every objectui merge group. ⛔ Not this PR's failure. Re-queued, unchanged, once objectui#11206's repair is on
|
Refs #11168
Clause-②: yes (narrowing) — breaking for an author who wrote the retired
action:group.name, an object ataction:group.actionsoraction:menu.actions, ormdataction:group.size: each is now refused by the manifest, the intrinsics and the page validator, as the spec already refuses it. The widening beside it is the spec keys the four blocks now publish as inputs. objectui's fixed group ships the break asminor, stated in the changeset. The PR waits as a draft for the director seat's contract review.Slice 1 of objectui#11168: the four
action:*blocks. Every guard row this card owns on these blocks was measured through the realSchemaRenderer, the renderer's own forward and the realActionRunner, against the installed@objectstack/spec17.5.0 rows, and decided under objectui#11111 decision 3 = B (record5902351047): declare what the block path honours, refuse or retire what it does not. The PR stays a draft for the director seat's contract review.What moved
inputs): onaction:buttonandaction:icon,visible,disabled,params,description,openIn,method,bodyExtra,bodyShape,operation,patch,confirmText,successMessage,errorMessage,refreshAfter,locations,toast,resultDialog,onSuccess,objectName, plusrecordIdFieldonaction:button; onaction:group,locationandvisible; onaction:menu,sizeandvisible.action:group.name.action:group.actionsandaction:menu.actionsfrom theobjectkind to a list of objects (type: 'array',of: 'object');action:group.sizefromsm/md/lgtodefault/sm/lg/icon.action:group.actions,action:menu.actions, and the ten array/object-armed keys each leaf block now publishes.endpointonaction:buttonandaction:icon,undoableonaction:button. The renderers keep forwarding both, so nothing changes at runtime.public-block-binding-reach.test.tsxderives its population from public blocks that publishobjectName. The two action leaves now do, and on them the key names the object an action is dispatched against, not a data binding. Both are listed and ledgered inNO_DATA_REACHwith that reason, which still asserts that the block fetches nothing under the name.@object-ui/components: minor) and one paragraph incontent/docs/guide/layout.md.Per-key verdicts
Read sites are cited by symbol and quoted expression. "Pin" names the row in
packages/components/src/renderers/action/__tests__/action-button-icon-inputs-11168.test.tsx(leaf blocks) oraction-group-menu-inputs-11168.test.tsx(containers). Every pin row carries a control in which the key is absent or holds the other value.action:buttonandaction:icon(one row per key, both blocks unless noted)visibleActionButtonRenderer/ActionIconRenderer:useCondition(toPredicateInput(schema.visible), recordData …)thenhasDeclaredVisibilityGate(schema.visible) && !isVisible; and theSchemaRenderernode gateevaluateVisibilityPredicate(newSchema.visible, 'visible')boolean/string/objectdisableduseCondition(toPredicateInput(schema.disabled), recordData)feedingdisabled={hostDisabled || (hasDeclaredVisibilityGate(schema.disabled) ? … )}boolean/string/objectparamsArray.isArray(schema.params) ? { actionParams: schema.params … };ActionRunner.executehands the list tothis.paramCollectionHandler(paramDefs, action)array(the list to collect; static values stay onproperties.params, and a node-level object is ignored under objectui#10289 ruling A)namedescriptiondescription: (schema as any).description; the consoleparamCollectionHandlerreadsaction?.description; onaction:iconalso the tooltipschema.label || schema.descriptionstringundefinedopenInexecuteUrl:const openIn = source.openIn ?? action.openInself/new-tabnew-tabgivesnewTab: true; control navigates in placeendpointexecuteAPI:action.api || action.endpoint || action.target; the consoleapiHandlerreadsconst target = action.target || action.nameand neverendpointapihandler the request goes to the endpoint; the census below finds no console readmethodexecuteAPI:method = action.method || 'POST'; console(action.method || 'POST').toUpperCase()stringPUT; controlPOSTbodyExtrabuildApiRequestBody:const bodyExtra = asRecord(action.bodyExtra); consoleObject.assign(body, …)objectbodyShapebuildApiRequestBody:const shape = action.bodyShape, then{ [wrap]: base ?? {}, ...bodyExtra }enum(flat) orobject{ wrap: 'data' }nests;flatand absent send the same flat bodyoperationisUpdateOperationAction:action.operation === 'update', thenexecuteUpdateOperationupdatepatchreaches thescriptdispatch; control withoutoperationmerges nothingpatchexecuteUpdateOperation:const params = { ...patch, ...collected }objectconfirmTextActionRunner.execute:if (action.confirmText)then the confirm handlerstringsuccessMessagehandlePostExecution:dyn || action.successMessage || …stringerrorMessagehandlePostExecution:action.errorMessage || result.errorstringrefreshAfterhandlePostExecution:if (action.refreshAfter && result.success) result.reload = truebooleanreload: true; control falsyundoable(button)executeUpdateOperation:if (action.undoable && rowRecord && writtenFields.length > 0); console runtimeaction.undoable && obj && recId && rowRecord;RecordDetailViewapiHandleraction.undoable && isThisRecord && pageRecordundo: null; the same def plus a host_rowRecordstash gets an undorecordIdField(button)resolveServerActionRecordId:recordId = selected[0]?.[recordIdField]stringcode; control sendsidlocationsresolveServerActionRecordIdand the consoleflowHandler:isRecordScopedAction(action)arrayofstringlist_toolbarrunstoasthandlePostExecution:const showToast = action.toast ?? { … }andaction.toast?.durationobjectshowOnSuccess: false,showOnError: falseanddurationeach asserted on the toast handlerresultDialoghandlePostExecution:this.resultDialogHandler(action.resultDialog!, result.data, action)objectonSuccesshandlePostExecution:readOnSuccessNavigation(action.onSuccess), thennavigateOnSuccessobjectnavigateinterpolated from the result,openIn: newTabopens a tab; control navigates nowhereobjectNamecreateServerActionHandler:action.objectName || resolveObject?.(action, context) || 'global'; console handlersaction.objectName || objApiNamestring/api/v1/actions/contact/…; control uses the page objectundoableandrecordIdFieldare not onaction:icon's spec row and its renderer does not forward them; the delivery probe showsundefinedfor both there, which is the probe's own lit zero.action:grouplocationActionGroupRenderer:declaredActions.filter(a => actionRendersAt(a, schema.location))ACTION_LOCATIONS)visibleuseCondition(toPredicateInput(schema.visible), recordData)andif (schema.visible && !isVisible) return null; theSchemaRenderernode gateboolean/string/objectnameschema.nameis never read; inline mode only spreads leftover props onto the wrapping div elementunrecognized_keysnamingname); the same members render identically with and without it, in both display modesactions(arm)const declaredActions: UIActionSchema[] = schema.actions || [], then.filterand.mapobjecttoarrayofobjectinvalid_typeatactions) and accepts a list; member pinsize(arm)schema.size === 'md' ? 'default' : (schema.size || 'default'); inlineaction.size === 'md' ? 'default' : (action.size || size || 'sm')default/sm/lg/iconmd(invalid_valueatsize); sizes asserted on inline members and the triggeraction:menusizeActionMenuRenderer:const size = schema.size || 'icon'default/sm/lg/iconlg; control icon-sizedvisibleuseCondition(toPredicateInput(schema.visible), recordData, { throwOnError: true, … })andif (schema.visible && !isVisible) return nullboolean/string/objectactions(arm)const actions: UIActionSchema[] = schema.actions || [], then.mapobjecttoarrayofobjectLedger arithmetic (
registry-inputs-spec-parity.test.ts)OBJECTUI_11111_LEDGER_CAPSunjudgedBlocksoffSpecInputsaction:group.name)unpublishedKeysrefusedArmsaction:*arms)element:definition-list,object-form)memberPinsaction:group.actions,action:menu.actions)element:*bookings)MEMBER_PIN_EXEMPTION_CEILING7 to 5;OFF_SPEC_EXEMPTIONSis pinned empty again.MEMBER_PINSgains 24 entries: the ten leaf keys on each ofaction:buttonandaction:icon(one claim per key, written once and spelled per block) andvisible/actionson each container.unpublishedKeyscap line and owner-count pin in parallel. Whichever PR lands second mergesmainand re-derives those two lines.Held keys
Each still carries its booked entry (reason text now states the measurement), so the guard keeps it visible.
endpoint(action:button,action:icon). The block forwards it and the runner's built-inapiexecutor reads it (probe: the request goes to the endpoint). The console registers its ownapihandler, and that handler readstarget, neverendpoint. On the console, anapiaction carryingendpointtherefore never calls it. The spec's ownActionSchemaalias table already mapsendpointtotarget. Publishing it would advertise a key the product drops.undoable(action:button). The block forwards it. The runner'soperation: updatepath, which is the spec's declared form of an update action, offers Undo only with a_rowRecordstash that hosts write and this block never does (probe:undo: null; the same def plus a stash gives an undo). The console runtime reads it under the same guard. Only the record page's ownapihandler honours it, and only for a logical target on that page.The options and a recommendation are in the report on the card.
Instruments
SchemaRendererinside a realActionProvider, with the button clicked and the value read where it arrives. All 20 forwardedaction:buttonkeys arrived intact, and all 18 ofaction:icon's. The probe's lit zero is icon'sundoableandrecordIdField, which arrive asundefined.visibleanddisabledare consumed by the renderer itself and are measured by the pin rows instead.runtimeReadKeys/forwardedKeysfromscripts/check-action-forward-parity.mjsover its four runtime consumers, withtargetandlabelas lit controls and a nonsense key as the zero.locationsread-on all four consumers. That is the census's documented blind spot (a read behind a helper):isRecordScopedActioninserverActionHandler.tsreads it, and the pin measures it directly.action-button-icon-inputs-11168.test.tsx(47 tests) andaction-group-menu-inputs-11168.test.tsx(20 tests). Every row mounts throughSchemaRenderer. Where the runner hands a key to a host handler, the handler is the realcreateServerActionHandlerfrom@object-ui/core(forobjectName,locations,recordIdField), or a recording double. Refusal rows assert the spec issue'scodeand path, not a baresuccess: false.Tests (commands from the repo root; verdicts read from vitest's own summary)
packages/components/src/renderers/ ui/ notifications/151 passed (1466 tests, 17 skipped);src/__tests__halves 94 passed plus 1 skipped, and 95 passed. All exit 0.scripts/__tests__/check-action-forward-parity.test.tsandpackages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: 45, 47 and 44 files passed. All exit 0.05d9cfdfa:registry-inputs-spec-parity,public-block-binding-reach, everything underpackages/components/src/renderers/action/,action-group.test.tsxand the forward-parity script tests: 33 files, 775 tests passed.@object-ui/componentstype-check(tsc --noEmit && tsc -p tsconfig.test.json) and@object-ui/consoletype-checkboth exit 0, after building each dependency closure.--listFilesOnlyshows both pin files in the components test project, and the guard and reach test in the console project.eslint --format jsonon the eight touched TypeScript files gives 0 errors. The warnings are pre-existingno-explicit-anyandonly-export-componentson untouched lines; the diff adds noany.eslint.config.jssets noparserOptions.projectorprojectService, so linting is not type-aware and the diff cannot move a verdict on an untouched file. Whole-packageeslint .is CI's.Ablations (each predicted before running;
ablation-replacewith anchor and blob proof, restore proven blob == HEAD withgit diff HEADempty)confirmTextonaction:button. Predicted: the published-input pin and the guard's reverse direction go red; the behaviour row stays green. Observed: exactly 2 red,every declared key is a published input …("action:button does not publish confirmText") andaction:button publishes every top-level key its spec props schema declares; 275 green. Anchor x1 to x0; blobfce9b14a0a47to97074e6ff9e1.mdonaction:group.size. Predicted: the size pin and the guard's arm direction go red. Observed: exactly 2 red,`size` publishes exactly the sizes the spec accepts …andaction:group declares no arm the spec refuses outright; 248 green. Blob26b31ec9a251to01c4266dad38.confirmTextfromaction:button's forward. Predicted: onlyaction:button's behaviour row goes red, andcheck:action-forward-paritygoes red. Observed: 1 of 47 red (`confirmText` is asked first …foraction:button), and the gate reports "does not forward 1 key the runtime reads:confirmText". Blobfce9b14a0a47to09421448fe69.Gates (at
05d9cfdfa, exit codes captured before any pipe)check-changeset-presence,check-changeset-no-major,check-changeset-fixed,check-changeset-overwrite,check-changeset-claims(report-only),check-pending-changeset-literals,check:control-bytes,check:new-line-citations(0 new),check:spec-symbols,check:action-forward-parity,check:doc-snippets(679 of 679 blocks, against a built closure),check:doc-types,check:prompt-keys,check:handler-key-reads,check:installed-pin-claims,check:test-path-roots,check:phantom-deps,check:unreferenced-sources,check:doc-example-readers,check:i18n-keys,check:element-data-source-declaration,check:esm-specifiers,check:side-effects-array,check:self-import,check:component-surface-parity(report-only; noinputsfinding on the four blocks).check-governed-queue-guard --teston the ten changed paths: NOT GOVERNED.check-spec-range-floors(reason: it needs a whole-workspace build, and the diff touches nopackage.json, which is the gate's only input).check:sdui-registration-pins(reason: it needs a console bundle, and the diff touches nosideEffectsarray). Repo-widepnpm type-check,pnpm testandpnpm lintare CI's.Acceptance notes (observations, not filed)
scripts/check-action-forward-parity.mjssays in its header thatlocations"is read only off the AUTHORED action … never off the forwarded def".isRecordScopedActioninpackages/core/src/actions/serverActionHandler.tsand the consoleflowHandlerread it off the forwarded def, andRUNTIME_CONSUMERSdoes not list that file. The gate is one-directional and every surface forwards the key, so this is dormant. Carrier: none.JUSTIFIEDentries forundoableonaction:icon/action:group/action:menureason only from the console runtime'srowRecordguard.RecordDetailView'sapiHandlerreadsaction.undoable && isThisRecord && pageRecordwith no stash. Unmeasured beyond that reading. Carrier: none.action-forward-parity.test.tsxheader callsrecordIdFieldinert on these renderers. This slice measures it honoured on thescriptpath with a single selection. Carrier: none.packages/types/src/zod/public-blocks.zod.tsstill says theaction:*blocks have "no spec row yet"; 17.5.0 carries them. Carrier: objectui#10872.@object-ui/typesexports anActionGroupinterface whose requirednameis the key this slice retires. Nothing imports it. Carrier: none.Base merge
mainat39e625de2is merged into this branch as80503f5bf, a merge commit with no rebase and no force-push, after PR #11184 (objectui#8649) landed as647908686. The one conflict was inapps/console/src/__tests__/registry-inputs-spec-parity.test.ts, in three hunks. Every figure was re-derived by counting the merged ledger's booked entries per ledger and per owner. The same census first reproduced each parent's own pinned figures as a control. The ledger table above is the slice's arithmetic at05d9cfdfa; these are the figures at the merged head.OBJECTUI_11111_LEDGER_CAPS:unjudgedBlocks4,offSpecInputs0,unpublishedKeys5 (objectui#11168 3, objectui#8652 2, objectui#8649 0),refusedArms2,memberPins4.MEMBER_PIN_EXEMPTION_CEILINGstays 5, because main did not touch that ledger.OBJECTUI_11111_OWNERSat 0. That is the convention its own landing used, and the file's header leaves removing the scaffold to the last owner card to land.UNPUBLISHED_EXEMPTIONSbooking docblock takes main's wording as landed.Both PRs' struck rows stay struck. The net diff against
mainis still the same ten files.80503f5bfpublic-block-binding-reach,packages/components/src/renderers/action/(both-11168pins),action-group.test.tsx, forward-parity script tests@object-ui/componentstype-checktsc --noEmit && tsc -p tsconfig.test.json@object-ui/consoletype-checktsc --noEmit && tsc -b tsconfig.node.json --forcecheck:action-forward-paritycheck-changeset-presence,-no-major,-fixed,-overwrite,-claims(report-only)check:control-bytescheck:new-line-citationsAblation:
unpublishedKeysset back to 14, this branch's pre-merge figure. Predicted and observed: exactly 1 red of the guard's 230 tests,the objectui#11111 ledger is capped at exactly the entries it lists("expected 5 to be 14"). Anchor x1 to x0, blobb26db645d11atof60e7157b89e; restored to blob == HEAD withgit diff HEADempty.The head moved, so a new contract-review record is owed before the queue.
Session
https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYmGenerated by Claude Code