Skip to content

fix(console): a create form asks its fields the create question, and eighteen write affordances read one affordance-to-grant map (objectui#12082) - #12084

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-12082-affordance-grant-map
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-12082-affordance-grant-map

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Part of #12082 (the seat's answer B, 6094765692: five write affordances that read no grant at all are this card's follow-up)
Part of #12081 (items 1, 5 and 6)
Clause-②: yes

A create form gated its fields on the EDIT grant, so a role that may create but not edit could not fill the form it is allowed to submit. This branch puts ONE affordance-to-grant map in @object-ui/core, read by the eighteen console write affordances it names (the five that read no grant at all are the card's follow-up, see Acceptance notes), and makes a create form ask the CREATE question, which follows the server's insert rule.

Implemented by the dispatched os-dev agent for the domain:ui#3 seat, session https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8. The dev wrote this body when the draft opened. The seat brought it to round 2 from the dev's report 6094735932 and the claim amendment 6094765692 on objectui#12082: the reader list, the pins' readings and the acceptance notes below are round 2.

Measured on main first

  • The card (p1), measured red on main 1b2d0160f. packages/plugin-form/src/createFormGrant-12082.test.tsx mounts every ObjectForm layout under MePermissionsProvider with the card's grant (allowCreate: true, allowEdit: false, no field entries). On main: every field of the create form drawn locked on all nine layouts, and the create body on the three one-step containers was {} (24 failed, 9 passed). With this branch: 33 passed. The empty body is the card's 400 VALIDATION_FAILED: the outbound filter asked the same write question as the render gate.
  • The server's insert rule (objectstack main 76bc1e03, read in source). plugin-security's middleware step "2.5. Field-Level Security write enforcement" runs on insert and update alike through computeForbiddenFieldWrites → FieldMasker.detectForbiddenWrites, which refuses only a field in getNonEditableFields — a field whose explicit entry is not editable. PermissionEvaluator.getFieldPermissions builds entries only for fields a permission set names, so a field with no entry passes the field step. Object admission maps insert → allowCreate (OPERATION_TO_PERMISSION). So a create-mode field reads: explicit entry → its editable; no entry → the object's create grant. That is exactly what checkField(object, field, 'create') now answers; no client rule the server lacks is added.
  • Item 1 (no header Edit for a caller whose update is allowed): the cause is server data, landed upstream. On objectui main the header Edit is the object's resolved edit affordance (managed-object policy ∧ effective API operations) ∧ the explain engine's record verdict ∧ the userActions.edit predicate. For hotclm's clm_payment_plan (sharingModel: 'controlled_by_parent', no managedBy, no userActions, no apiMethods) the only principal-dependent input is the explain record verdict, which useRecordEditable reads as record.visible. fix(plugin-security): explain's update verdict on a controlled_by_parent record comes from the master-detail write check objectstack#22529 (the pull request for security(explain): POST /api/v1/security/explain answers allowed for an update of a controlled_by_parent record whose own PATCH refuses — explain asks sharing's canEdit, which reads controlled_by_parent as org-shared objectstack#22514, merged 2026-10-09, after the 17.7.0 tag — not an ancestor of the tag commit 4e4e8814, control leg: the tag's 30th ancestor answers exit 0) made explain's update verdict on a controlled_by_parent record come from the master-detail write check; its own reproduction table shows the hotclm shape (allowed: true, record.visible: false, PATCH 200). plugin-security: the record-grained explain verdict for update is not computed with the write path's inputs — record.visible is false on rows the by-id PATCH admits, so every consumer hides Edit from permitted users objectstack#19963 is the older card on the same shape. Nothing on objectui's side hid Edit from that caller; the map's recordEdit row now also reads the update grant, and the pin holds that a caller with allowEdit and update in the operation set gets Edit.
  • Item 5 (New / Import with allowCreate: false): does not reproduce as stated. Both list surfaces (ObjectView, ObjectDataPage) already ANDed can(object, 'create') on main and in 17.7.0. hotclm binds every position to clm_requester as well as to its own set (src/security/bind-position-sets.ts), and clm_requester grants allowCreate: true on clm_contract, so the records manager's EFFECTIVE grant allows create: the server would accept the create, and New / Import were right to show. The rows are pinned anyway, now read through the map.
  • Item 6 (lookup "Create new" without create on the target): reproduces on main by source. LookupField's canCreate read no grant at all. It now reads the lookupCreateNew row, asked of the target object.

The map

packages/core/src/utils/affordanceGrants.ts: AFFORDANCE_GRANTS names each affordance's CRUD-affordance bit, the object grant it exercises and, for an affordance that offers fields, the field question (create = insert rule, write = update rule). resolveAffordance is the one verdict — managed-object policy ∧ the server's effective API operation set (getObjectApiOperations, kept inside the map) ∧ the caller's grant — with the row's userActions predicates surfaced only when all three allow it. resolveFieldAffordance asks a field the row's question.

Why core, not permissions. Every reader already depends on both packages, and resolveEffectiveCrudAffordances lives in core. permissions depends on @object-ui/types alone; homing the map there would add a permissions → core edge. In core the principal is a structural interface (can, getObjectApiOperations, checkField, isLoaded), so core gains no edge either.

Fail-open stays. With no permission provider mounted, usePermissions() answers can with true and isLoaded with false: every grant reads open and no field question is asked, as the fieldWriteGate.ts docblock states.

Readers

Rows and readers (the census of resolveEffectiveCrudAffordances, isObjectInlineEditable, CRUD-verb can( / check(, and checkField(…, 'write') readers) — each becomes a map reader or is named below as outside the family:

  • create-form fields / edit-form fields and the form-wide lock — every ObjectForm layout through fieldWriteGate.ts (render pass, outbound filter, closedFormAffordance);
  • record-header Edit / Delete and the record body's in-place editing — RecordDetailView resolveRecordHeaderActionGates;
  • list New / Import — ObjectView, ObjectDataPage; the import wizard's writable target fields ask the create question (importTargetFields);
  • lookup "Create new" — LookupField, on the target object;
  • create-mode MasterDetailForm line cells ask the create question of the child object, since every line there is a new record;
  • the other census readers, each now a map reader: RelatedRecordActionsBridge (related lists), RecordAttachmentsPanel (Upload / delete on sys_attachment), plugin-list ListView (bulk Delete, inline-edit toggle), plugin-grid ObjectGrid + rowCrudAffordances (row Edit / Delete, in-place editing, the add-record row; rowCrudAffordances now takes the map's verdicts) and ImportWizard (template download), plugin-detail DetailView (operation set + grant; the object's own policy stays the host's channel per objectui#4419) and record:details (in-place editing reads the update grant), and the console ProfilePage (the language field).

Out of the family, with the reason: managedByEmptyState (empty-state copy from the managed-object bucket; it shows or hides nothing); useFieldPermissions canWrite / writableFields (the resolver's own API); the read gates (checkField(…, 'read')).

Behaviour moves beyond the card's rows, all toward the server's refusal: the grid's add-record row now also honours the managed-object policy and the effective create operation; DetailView's object gate adds the effective operation set; record:details in-place editing reads the update grant; the form-wide lock and its notice also engage on a denied grant for the form's mode.

Pins

Readings (dev report 6094735932):

  • measured red on main 1b2d0160f first: the card's pin 24 failed / 9 passed (every field locked, create body {}); the item-6 pin against main's LookupField 3 failed / 2 passed (controls green).
  • at the branch head: createFormGrant-12082 35/35, affordanceGrantMap-12082 27/27, LookupField.createGrant-12082 5/5; the family pins' final run 161/161.
  • ablations (scripts/ablation-replace.mjs, predictions first, every restore blob-equal): A1 the create form's field question back to 'write' ⇒ 19 red; A3 a grant read planted outside the map in LookupField ⇒ the census names that file; A4 recordEdit's grant swapped ⇒ 4 red; A5 MasterDetailForm forced to edit mode ⇒ 1 red.
  • the Console's eager closure: +655 B gzipped (3,244,032 → 3,244,687 B over 290 eager chunks); check:eager-closure passes at the head.

Acceptance notes

  • The family's remainder (the card's follow-up, seat answer B 6094765692): five write affordances read no grant at all, so the grep census over grant reads could not see them: plugin-view ObjectView's create button, plugin-calendar's quick-create and drag-to-reschedule, plugin-kanban's card move, and plugin-form LineItemsPanel's add / remove lines. The census pin refuses grant reads outside the map; it does not catch an affordance that reads no grant, and the follow-up says so or closes it.
  • objectui#12081 items 1 and 5, carried to the hotclm seat (noted, not filed): item 1's cause is upstream (fix(plugin-security): explain's update verdict on a controlled_by_parent record comes from the master-detail write check objectstack#22529, after the 17.7.0 tag), so hotclm's finance header Edit is re-measured once its @objectstack pin includes that merge; item 5's New / Import are correct for that user's effective grant.
  • MePermissionsProvider.check keys objects by the name as given, while checkField and getObjectApiOperations lowercase it first. Every reader passes the object's own (lowercase) name, so nothing diverges today; noted, not changed.
  • LookupField reads allow_create ?? allowCreate (two spellings of one key) — outside this card, noted.

Generated by Claude Code

…create question, through one affordance-to-grant map (objectui#12082)

A create form gated its fields on checkField(..., 'write'), whose fallback
for a field the permission set does not mention is allowEdit, so a
create-only role met a create form with every field disabled and a save
that posted an empty body.

- core: AFFORDANCE_GRANTS + resolveAffordance / resolveFieldAffordance /
  formFieldsAffordance, the one map every affordance reads.
- permissions: checkField accepts 'create' (explicit entry, else
  allowCreate), the server's insert rule.
- plugin-form: every layout's field gate, outbound filter and form-wide
  lock read the form's row in the map.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…affordance-to-grant map (objectui#12082)

- RecordDetailView: resolveRecordHeaderActionGates takes the caller's
  permissions and resolves the recordEdit / recordDelete rows, so Edit
  and Delete read the update / delete grant (before: none at all).
- ObjectView / ObjectDataPage: New and Import are the listNew /
  listImport rows (policy, effective operations and the create grant in
  one verdict, predicates only when it allows).
- importTargetFields: the wizard's write targets ask the create question,
  so a create-only caller offered Import keeps its insertable fields.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
… grant (objectui#12082)

The built-in quick-create read whether the field offers it and whether a
host can carry it out, and no grant. It now reads the lookupCreateNew row
of the affordance-to-grant map, asked of the referenced object (policy,
effective operations and the create grant), so a caller who cannot create
the target is not offered "Create new" (objectui#12081 item 6).

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
… the affordance-to-grant map (objectui#12082)

RelatedRecordActionsBridge resolves the child's relatedNew /
relatedRowEdit / relatedRowDelete rows and RecordAttachmentsPanel the
attachmentUpload / attachmentDelete rows, instead of composing policy,
operation set and grant each on its own. Same verdicts as before.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
… affordance-to-grant map (objectui#12082)

- ListView: bulk Delete and the inline-edit toggle are the listBulkDelete
  / listInlineEdit rows.
- ObjectGrid: row Edit / Delete, in-place editing and the add-record row
  are the rowEdit / rowDelete / listInlineEdit / gridAddRow rows, resolved
  against one source; resolveRowCrudAffordances now takes the map's
  verdicts instead of composing bucket, operations and grant itself.
- ImportWizard: the template download is the importTemplate row.

The add-record row now also reads the object's managed-object policy and
effective create operation (the gridAddRow row), not only the grant.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…e affordance-to-grant map (objectui#12082)

- DetailView: its object-level Edit / Delete gate is the recordEdit /
  recordDelete row (operation set and grant); the object's own policy
  stays the host's channel, as objectui#4419 ruled.
- record:details: in-place editing is the recordEdit row, so it now also
  reads the caller's update grant (before: bucket and operation set
  only), the same row the console header's Edit reads.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…ap; a create-mode master-detail grid asks the create question (objectui#12082)

- affordanceGrantMap-12082.test.tsx: the map's rows held to a hand-written
  expectation table; every row x four grant shapes (create-only,
  edit-only, read-only, full) through the real MePermissionsProvider; the
  field questions; the form reader; fail-open; and a census of console
  sources refusing a CRUD grant read outside the map.
- MasterDetailForm: a create form's line cells ask the create question of
  the child (every line is an insert); edit forms keep the edit question.
- console ProfilePage: the locale field reads the editFormFields row.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the apps label Oct 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3168.6 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-BB2Y1vSH.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 587.83KB 141.44KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 14.32KB 5.17KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.19KB 65.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.30KB 45.92KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.09KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.93KB 23.24KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…he create question (objectui#12082)

Document AFFORDANCE_GRANTS / resolveAffordance / resolveFieldAffordance
in the core README, checkField's 'create' action in the permissions
README, and the create-form field question in the plugin-form guide;
the changeset now names every package whose source moved.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Oct 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3168.6 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-BB2Y1vSH.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 587.83KB 141.44KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 14.32KB 5.17KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.19KB 65.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.30KB 45.92KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.09KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.93KB 23.24KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…nt-mask.mjs (objectui#12082)

The enumeration pin's census stripped comments with a private regex, which
check-hand-rolled-comment-mask refuses (a regex opens phantom comments on
a glob or a URL and reports clean over code it never read). It now uses
the shared stripComments, typed locally as the other package tests do.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3168.6 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-BB2Y1vSH.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 587.83KB 141.44KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 14.32KB 5.17KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.19KB 65.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.30KB 45.92KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.09KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.93KB 23.24KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet objectstack-fleet Bot changed the title fix(console): one affordance-to-grant map every affordance reads — a create form asks the create question (objectui#12082) fix(console): a create form asks its fields the create question, and eighteen write affordances read one affordance-to-grant map (objectui#12082) Oct 10, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 62413c9863b425d2ea2f6bd2ba7f7715cf828236
Local-runs: none

Inputs read: card #12082 (body and its four comments — triage 6093495104, claim 6093729821, dev report 6094735932, claim amendment / seat answer 6094765692); PR #12084 (draft, open: body, the 43-file list, the net diff from merge-base 1b2d016 to the head, +1714/-284, identical to the three-dot diff against origin/main at 12ff256); the 43 check-runs on the head (41 success, the two coverage matrix stubs and dependabot skipped; Vercel status success). Head-tree facts below were read with git show / git grep against the fetched head and the cited objectstack commit; nothing was built, run or re-run.

① Derived judgments

Gate verdicts on the head, as the check-runs answer them: Type Check, Lint, Test (shards 1–8 and dist pins), Build & E2E, Build Docs, Bundle Analysis, Changeset Declaration / Bump Policy / Claim Re-read / Fixed Group Check / Overwrite Report, README Export Check, Pre-Install Import Graph Check, Docs Route Eager Closure Check, Governed Surface Queue Guard, Line Citation Gate, Inert vi.mock Specifier Check, Control Byte Scan, Shell Escape Residue Scan, the four doc checks, Internal Docs Link Check, Skill checks, Spec Main Shape Gate, Action Ref Convention, Live E2E (informational), label — every one success. No red check-run on this head; the shard-4 red the dev reported at fc8e019 is not on it.

The accept-set and public-surface changes the diff implies, each judged:

  1. @object-ui/core — src/index.ts adds export * from './utils/affordanceGrants.js': AFFORDANCE_GRANTS, resolveAffordance, resolveFieldAffordance, formFieldsAffordance and nine types become public. RIGHT, additive. The 18 rows name the grant each affordance exercises exactly as the card's direction lists them (create-form fields → create, edit-form fields → update, header Edit → update, New / Import → create, lookup "Create new" → create on the TARGET) plus the census rows (recordDelete, listBulkDelete, rowDelete, relatedRowDelete, attachmentDelete → delete; listInlineEdit, rowEdit, relatedRowEdit → update; gridAddRow, relatedNew, importTemplate, attachmentUpload → create). Each is the grant the server's object admission reads for that write. resolveAffordance is policy ∧ effective operation set ∧ grant, predicates surfaced only when all three allow — a predicate never re-opens what a layer closed. RIGHT.
  2. @object-ui/permissions — checkField's action union widens to 'read' | 'write' | 'create'; MePermissionsProvider answers 'create' from the explicit field entry when one exists and from allowCreate otherwise (an unknown object keeps the authentication-gated default); the role-based PermissionProvider answers it as 'write'. RIGHT. Verified against objectstack 76bc1e03 plugin-security: step "2.5. Field-Level Security write enforcement" runs when the operation is insert or update, FieldMasker.detectForbiddenWrites refuses only a field whose explicit entry is not editable ("Fields without a permission entry pass through"), and OPERATION_TO_PERMISSION maps insert to allowCreate. The create question adds no client rule the server lacks; the card's "measure first" direction is met. The widened union reaches the public surface only through usePermissions()'s return type — PermissionContextValue and PermCtx are not exported from the package index — so no external implementer is narrowed.
  3. @object-ui/plugin-form — fieldWriteGate(perms, objectName, mode) gains a required mode, FormFieldPrincipal gains a required can, applyColumnPermissions gains an optional mode. None of fieldWriteGate, gateFormFields, applyFieldPermissions, applyColumnPermissions, closedFormAffordance or the two principal types is exported from packages/plugin-form/src/index.tsx: internal, no public accept-set change. RIGHT at patch. All six layout call sites pass schema.mode (ObjectForm, DrawerForm, ModalForm, SplitForm, TabbedForm, WizardForm), and the master-detail route gates the parent's fields with the raw base.mode before routing. formFieldsAffordance keys strictly on mode === 'create'; a form with no declared mode keeps the pre-PR edit question and no lock, as the docblock states. ObjectFormSchema.mode is a required member in @object-ui/types (objectql.ts), and every console construction sets it — ScreenView (the card's flow-screen surface), AppContent, useActionModal (|| 'create'), RecordFormPage, ViewPreview, StudioDesignSurface — so the create question reaches every console create surface. RIGHT.
  4. @object-ui/app-shell — resolveRecordHeaderActionGates(objectDef, perms) changes its second parameter from an operation set to an AffordanceGrantPrincipal; ImportFieldPerms.checkField widens. Neither symbol is exported from src/index.ts (views/index.ts exports RecordDetailView alone): internal. RIGHT at patch. The header's Edit / Delete now also read the update / delete grant, which this gate read nowhere before; item 1's pin holds that a caller with allowEdit and update served gets Edit.
  5. @object-ui/plugin-grid — resolveRowCrudAffordances drops managedBy / userActions / effectiveApiOperations / permissionUpdate / permissionDelete for edit / delete verdicts. Not exported from src/index.tsx; its only reader is ObjectGrid. RIGHT at patch. The add-record row now also honours the managed-object policy and the effective create operation — toward the server's refusal, and flagged.
  6. @object-ui/fields LookupField — "Create new" now ANDs the target object's lookupCreateNew verdict; a host onCreateNew stays the host's. RIGHT (item 6, red on main by the dev's reading, pinned with controls). @object-ui/plugin-detail — DetailView keeps objectSchema: null so the object's own policy stays the host's channel (objectui#4419) and adds only the operation set; record:details adds the update grant, the same row as the header. RIGHT. @object-ui/plugin-list bulk Delete / inline edit and @object-ui/console ProfilePage read their rows with the verdicts they had. RIGHT.
  7. Fail-open with no provider mounted — the claim's "not on it": usePermissions()'s fallback answers can with true, isLoaded with false, checkField with true and getObjectApiOperations with undefined; resolveFieldAffordance asks nothing when isLoaded is false; resolveAffordance reads can, which is true. Unchanged. RIGHT. A mounted MePermissionsProvider renders loadingFallback instead of its children while its data is null, so its fail-closed check (permissions-loading) never reaches a map reader, and a refetch answers from the previous map. No loading-state regression.
  8. The enumeration pin and the census — every row × four grant shapes through the real provider, the row set held to a hand-written table, the three field rows over explicit entries, the form reader in both modes, and a tree walk refusing can(x, write-verb), checkField(x, f, 'write' | 'create'), resolveEffectiveCrudAffordances( and isObjectInlineEditable( outside the map's two home modules and packages/permissions/src, with one OUT_OF_FAMILY entry. At the head canWrite / writableFields have no consumer outside packages/permissions/src and useRecordEditable reads no grant, so the out-of-family list is complete. The pin cannot see an affordance that reads no grant; that is the acknowledged remainder (③).
  9. objectui#12081 item 1 — objectstack#22529 merged 2026-10-09T20:33Z; its merge commit d303b3e7 is on objectstack main and is NOT an ancestor of 4e4e8814, which @objectstack/console@17.7.0 resolves to. The "upstream, after the tag" reading is RIGHT. Item 5's effective-grant reading rests on hotclm metadata outside this review's inputs; it is carried to the hotclm seat as the body says, and the New / Import rows are pinned through the map regardless.
  10. Behaviour moves beyond the card's rows — the grid add-row's policy ∧ operation, DetailView's operation set, record:details' update grant, the form-wide lock and its notice on a denied grant for the form's mode, and create-mode master-detail line cells asking the child's create question — each narrows toward the server's refusal and is named in the PR body. RIGHT. One limit on record, documented in the diff: an edit form's line cells keep the edit question even for a newly added line (one lock per column), stricter than the server's insert rule, not wider.

② Semver level

.changeset/12082-affordance-grant-map.md: @object-ui/core minor, @object-ui/permissions minor; plugin-form, app-shell, fields, plugin-list, plugin-grid, plugin-detail, console patch. Matches what the diff publishes: the two widened public surfaces (the map's exports; checkField accepting 'create') are minor, and every other package moves behaviour behind unchanged public exports (① items 3–6). No major, as objectui AGENTS.md §9 requires; the Changeset Bump Policy and Fixed Group checks are green on the head. The nine released packages whose src/ the file list touches (core, permissions, plugin-form, app-shell, fields, plugin-list, plugin-grid, plugin-detail, apps/console) are the nine the changeset names. Clause-②: — yes on the PR body's third line, yes in the claim 6093729821, "yes (widening)" in the changeset body, held by the amendment 6094765692: consistent, and the widening is real. The PR body opens Part of #12082 / Part of #12081 with no closing keyword, as seat answer B requires.

③ Boundary flags

  • open_questions[0] — five write affordances that read no grant (plugin-view ObjectView create button, plugin-calendar quick-create and drag-to-reschedule, plugin-kanban card move, LineItemsPanel add / remove lines): ANSWERED by the seat, B (6094765692) — a follow-up dispatch on this card after landing, the PR no longer closes the card, and the follow-up must state the census's limit. The body's Acceptance notes carry it.
  • deviation: the PR body written once at draft open — ANSWERED: the seat brought it to round 2 (Readers, Pins and Acceptance notes are in the body as read).
  • deviation: files beyond the claim's named surface (plugin-detail DetailView / record-details, plugin-grid ImportWizard / rowCrudAffordances, console ProfilePage and its three test stubs, MasterDetailForm, the core and permissions READMEs, plugin-form.mdx) — ANSWERED: amendment §2 adds each to the file surface.
  • deviation: behaviour moves beyond the card's rows — ANSWERED by inclusion in the round-2 body; judged in ① item 10.
  • deviation: the census is a test-level pin, no new check:* gate — ANSWERED by the card's own direction, which asked for an enumeration pin.
  • deviation: a root-level test missed locally, shard 4 red at fc8e019 — CLOSED: fixed on this head, every Test shard green.
  • deviation: add_repo read of hotclm with an anonymous shallow clone in the scratchpad, and a temporary ref refs/os-dev-12082/main written to the shared .git and deleted — not addressed by the seat; no trace in the diff or on the head. ESCALATED as a process note for the seat's acknowledgement only: a ref write in a shared checkout is outside a dev's lane even when reverted.
  • deviation: model-free commit trailers — per objectui AGENTS.md; no action.
  • out_of_scope_findings: the item 1 and item 5 carriers — the seat records them on objectui#12081 at landing (amendment §3); check keying objects as given while checkField lowercases, allow_create ?? allowCreate, the ObjectGrid cross-file line address — noted, not filed, named in the body's Acceptance notes; none bears on this head.
  • The claim's "not on it" held: no server, spec or envelope change; no packages/components/src/ui/** file; fail-open unchanged (① item 7); objectui#12081 items 2, 3, 4, 7 and 8 untouched.

Implemented-by: claude/issue-12082-affordance-grant-map
Reviewed-by: session_01CGZy1BGCjdN5cXqL9cnvB8

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 07:02
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 07:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit 023f00d Oct 10, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12082-affordance-grant-map branch October 10, 2026 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants