Skip to content

fix(console): five write affordances that read no grant now read the affordance-to-grant map, and a write census holds every write call site to a row (objectui#12082) - #12094

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

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-12082-write-affordance-census

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #12082 (the five affordances and the write census; the census's 21 unmapped write sites stay on the card as its remainder, per the seat's answer C)
Clause-②: yes

The card's remainder, as triage scoped it in comment 6095316139: five write affordances that read no grant at all now read the affordance-to-grant map, and the map's enumeration pin gains a census of WRITE call sites, so an affordance that reads no grant can no longer hide from it. Dispatched by the domain:ui seat 3 claim 6095684352, session https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z.

What changed

Affordance Row How a closed row presents
object-view toolbar New (@object-ui/plugin-view ObjectView, renderToolbar) listNew (reused: it is an object list's New) button absent
Calendar quick-create: an empty-day click (handleDateClickDefault) and a week / day range drag (handleTimeRangeSelectDefault) calendarQuickCreate (new: create) onDateClick / onTimeRangeSelect not handed to CalendarView, which draws the affordance from handler presence: no dialog, no range drag
Calendar drag-to-reschedule (handleEventDropDefault) calendarReschedule (new: update) onEventDrop not handed: events not draggable
Kanban card move (handleCardMove / persistCardMove) kanbanCardMove (new: update) cards drawn not movable (dnd-kit sortable disabled, aria-disabled), and the board gets no mover
record:line_items add a line (Add, entry row, Duplicate) / remove a line relatedNew / relatedRowDelete on the CHILD (reused: the same two writes a related list makes) allowAdd / allowDelete false
  • Each reads resolveAffordance (object policy, effective API operation set, caller's grant). With no permission provider mounted every one reads open, as the map defines (fail-open unchanged).
  • A host-supplied onDateClick / onEventDrop on ObjectCalendar is the host's channel and is handed through unchanged.
  • Kanban: a new internal prop cardsMovable on KanbanBoardCore and the lazy board (read through a context by every card); never a schema key; default true, so KanbanRenderer hosts are unchanged.
  • PM mechanism assumption 1, measured: the time-range quick-create exists, as handleTimeRangeSelectDefault in ObjectCalendar.tsx, wired to CalendarView's onTimeRangeSelect. Assumption 3 held: the line rows' semantics match relatedNew / relatedRowDelete, so no new rows there.

The write census (closing the limit, not stating it)

In packages/plugin-form/src/affordanceGrantMap-12082.test.tsx, beside the existing grant-read census:

  • Population: the same console sources (packages/*/src, apps/*/src, tests excluded), parsed with the TypeScript parser. A write call site is a member call naming a DataSource write (create, update, delete, bulk, bulkUpdate, bulkDelete, batchTransaction) with at least the interface's arity, which separates ds.delete(o, id) from a Map / Set delete(key), or a call of a write helper (runBatchTransaction, emulateBatchTransaction, recordDelete.run, saveWithOcc).
  • Key: FILE :: enclosing symbols :: verb (symbol-keyed, never a line address).
  • Ledger WRITE_SITES: every site has an entry of one kind: mapped (rows, and the files that read them), helper, door (a DataSource implementation or engine), outOfFamily (with the reason) or unmapped (a write affordance with no row yet, named one by one).
  • Assertions: the site set equals the ledger both ways (a new site with no entry is red, naming it; an entry whose site is gone is red); every mapped entry names map rows of its own verb, each read by a file it names; the helper table and the helper entries name each other; a recognizer control on a synthetic source (including a collection delete(key) and prototype-named calls that must not count).
  • What it does not see, stated in the pin: a write that bypasses the DataSource door with raw HTTP (AGENTS.md Implement visual designer for Object UI schemas #1), and an affordance that only leads to a write (a button opening a form; the form's submit is a site, the opener is pinned by its own runtime test).
  • Reading at d18c611d2: 93 sites. 33 mapped, 24 door, 6 helper, 9 out of family, 21 unmapped. (The instrument is the pin; these counts are a reading at that commit.)

Measured on main first

The four runtime pins were run against the BASE source (023f00d4, the seven source files checked out from it under a restore trap, markers confirmed absent, restore proven: git diff HEAD empty and all seven blobs equal HEAD): 15 failed, 15 passed. Every red is the defect direction (the affordance offered to a caller whose grant denies it: expected true to be false, onDateClick: expected [Function] to be undefined, aria-disabled 'false' where 'true' was due, the New button present); every allowed shape and fail-open leg was green on main too.

Tests (at d18c611d2 unless noted)

  • New pins: ObjectView.createGrant-12082 (plugin-view), ObjectCalendar.writeGrant-12082, ObjectKanban.moveGrant-12082, LineItemsPanel.lineGrant-12082, plus the census in affordanceGrantMap-12082: 5 files, 63 passed.
  • Package suites: plugin-form 172 files (2081 passed, 1 skipped); core 199 files plus plugin-grid rowCrudAffordances.test.ts (200 files, 3995 passed, 27 skipped); plugin-view 75 files (698 passed) and plugin-kanban + plugin-calendar 128 files (911 passed, 95 skipped), both at 02cc3c82f (the merge of main since then touched only app-shell and this branch's census ledger).
  • type-check (with the hyphen; each echoed its script name, 0 error TS): core, plugin-kanban, plugin-calendar, plugin-form, plugin-view at 02cc3c82f.
  • Ablations through ablation-replace.mjs (WRAP mode, predictions written first, every restore proven blob == HEAD and git diff HEAD empty), on ObjectKanban.tsx:
    • A1, gate removed (mover always handed, cards always movable): kanban pin 3 failed (create-only, read-only, operation-set legs), census green. As predicted.
    • A2, the site reads rowEdit instead of kanbanCardMove (same verb, so runtime identical): kanban pin green, census 1 failed, naming persistCardMove: no file it names reads kanbanCardMove. As predicted.
    • A3, a planted dataSource.delete in a new handler: census 1 failed, unlisted naming ObjectKanban > archiveCard :: delete. As predicted. The first A3 attempt was a no-op (its replacement re-contained the anchor, so the tool refused it with anchor 1 to 1 and restored); it was redone with a replacement that does not contain the anchor.
  • Gates: check:control-bytes, check:new-line-citations (0 new), check-changeset-presence / -claims / -no-major / -overwrite, check:pending-changeset-literals, check:vi-mock-specifiers / -inherit / -override-shape, check:test-path-roots, check-hand-rolled-comment-mask at d18c611d2; check:doc-links, check:doc-fences, check:phantom-deps, check:self-import, check:esm-specifiers, check:unreferenced-sources, check:handler-key-reads, check-type-check-coverage, check-lint-coverage, check-changeset-fixed at 02cc3c82f: all exit 0.
  • eslint on the 12 touched source and test files: 0 errors; 0 warnings on added source lines; 6 no-explicit-any warnings in the new test files, the sibling tests' fixture idiom.
  • NOT MEASURED: check:eager-closure (needs a base and a head console build; the eager-closure delta of three const rows in @object-ui/core is left to the Bundle Analysis workflow); check:readme-exports (the run's population collapsed with 21 packages unbuilt in this worktree, a prerequisite, not a verdict; the core README edit is prose with no import binding); the full scripts/__tests__ suite (CI).

Acceptance notes

  • The census found a backlog beyond the five. 21 unmapped sites, each named in the ledger with its reason:

    • the record feed's comment / reply composer and reaction toggle (sys_comment, sys_comment_reaction), 5 sites;
    • Setup's permission-set assignment (sys_user_permission_set create / delete), 2;
    • the record overlay's inline field edit in calendar, kanban and gantt: RecordDetailPanel opens its inline-edit session on handler presence alone, so a caller without update is offered the editors (the overlay's Delete is behind recordDelete in DetailView), 3;
    • gantt drag / resize, dependency edit and row Delete, 3;
    • the dashboard designer's Save (sys_dashboard), 1;
    • a related list's Add existing (junction create, re-parent update) and its fallback remove with no host handler, 3;
    • MasterDetailForm's line grids' add / remove (a create-mode master's removed line is never a server delete, so the line rows do not apply as-is), 1;
    • a grid's bulk field update, with its undo and retry, 3.

    These are recorded, not taken: per the dispatch, a large unmapped backlog is a decision for the seat, not silent scope growth. Each needs its reach measured first.

  • ImportWizard's legacy per-row create is behind listImport on the console list; the AI build panel's spreadsheet import (ExcelImportBar) mounts the same wizard with no grant read (noted on that ledger entry).

  • The card move and the reschedule ask the object grant, not the field question on the field they write (groupBy, the date fields); a field the permission set marks editable: false is still refused only by the server.

  • A line-items panel under a create-only child grant: Add is offered, while the grid's cells ask the edit question for every row (the column gate is per column, not per row), so a new line's cells are locked. That is unchanged by this PR; a per-row create question needs per-row column gating in GridField.

  • A remove-only gate is whole-grid: under a grant without delete, a just-added, unsaved line cannot be removed before Save either.

Changeset

.changeset/12082-write-affordance-census.md: @object-ui/core minor (three new rows widen the exported AFFORDANCE_GRANTS and the ConsoleAffordance union; nothing removed or renamed); plugin-view, plugin-calendar, plugin-kanban, plugin-form patch.


Generated by Claude Code

…p's four grant shapes (objectui#12082)

The object-view create button, the calendar quick-create and drag-to-reschedule,
the kanban card move and the line-items panel's add / remove, each measured
through the real MePermissionsProvider over create-only, edit-only, read-only
and full, plus fail-open and the effective operation set.

Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z
Co-authored-by: Claude <noreply@anthropic.com>
…affordance-to-grant map, and a write census holds every write call site to a row (objectui#12082)

- object-view's New reads listNew; the calendar's quick-create reads the new
  calendarQuickCreate row and drag-to-reschedule calendarReschedule (a closed
  row withholds the handler CalendarView draws the affordance from); the
  kanban card move reads the new kanbanCardMove row (cards not movable, no
  mover handed to the board); a line-items panel's add / remove read
  relatedNew / relatedRowDelete on the child.
- The enumeration pin gains a write census: every create / update / delete
  call site on a data source (or a write helper wrapping one) is keyed by
  file, enclosing symbols and verb, and has an entry naming the map row it
  sits behind and the file reading it, or why it is not a row's, or that it
  is an unmapped write affordance (the backlog the census found).

Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z
Co-authored-by: Claude <noreply@anthropic.com>
…gin docs; changeset; the census reads its helper table by own key (objectui#12082)

Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z
Co-authored-by: Claude <noreply@anthropic.com>
…ctui#12078 landed on main (objectui#12082)

The merge of main brought the chatter's sys_comment_reaction create / delete
into RecordDetailView's reaction toggle; both are entered as unmapped write
affordances beside the legacy sys_comment update they sit next to.

Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z
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) 3169.6 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-wDKi3920.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.66KB 15.64KB
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.59KB 46.00KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 53.23KB 16.71KB
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) 92.08KB 23.26KB
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

Copy link
Copy Markdown
Contributor Author

Spec Main Shape Gate red on d18c611d2f is not this PR's · domain:ui seat 3, session_01B1gHb9baeX7oioD5sHVm7z, 2026-10-10T13:36Z

  • The failing check: Spec Main Shape Gate, check run 114223170489.
  • Why it is not this PR's:
    • The annotation set is identical, file and line, to the one anchored on objectui#12093: page-variables.test.tsx:213, record-picker-label-association.test.tsx:170, authoring-nodes-11364.test.ts:131/:143, element-repeater-data-source-11880.test.ts:242/:247, and grid-default-filters-gantt-map-filter-round10-6152.test.ts:219/:220/:234/:235.
    • None of those files is in this PR.
    • The cause is objectstack PR #22421 (317cddd40d), which retired the flat element binding keys and object-grid.defaultFilters. The gate is red for every objectui PR.
  • The fix: anchored on objectui#12093 and folded into objectui#12085's in-flight dispatch. It is not on main yet. No re-run is spent.
  • This PR: stays a draft. The seat's own review and the contract-review-tier review this Clause-②: yes owes run on this head. Landing waits for objectui#12093's fix on main, a merge of main, and every check green.
  • Seat note on the body: the first line now reads Part of #12082. The census's 21 unmapped write sites stay on the card (the seat's answer C to the dev's open question).

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d18c611d2f98300f43d90de1102cffe553f7ec8e
Local-runs: none

① Derived judgments

  • The three new rows, each against the write it gates — RIGHT.
    • calendarQuickCreate { crud: 'create', grant: 'create' } gates ObjectCalendar > submitQuickCreate :: create. A closed row withholds onDateClick and onTimeRangeSelect, from which CalendarView draws both affordances.
    • calendarReschedule { crud: 'edit', grant: 'update' } gates handleEventDropDefault :: update. A closed row withholds onEventDrop, which CalendarView reads for draggable and the resize handles.
    • Both are asked of schema.objectName, the object both handlers write. A host-supplied handler is passed through, which is the host's channel, and that is pinned.
    • kanbanCardMove { crud: 'edit', grant: 'update' } gates persistCardMove :: update. A closed row hands the board no onCardMove and draws the cards not movable (useSortable is disabled, aria-disabled is set).
    • Each row has the shape of rowEdit / recordEdit (object-level grant, no field). The field-level question stays the server's refusal.
  • The reuses — RIGHT, with one residual named.
    • listNew serves the object-view toolbar New, ANDed onto the node's toggles.
    • relatedNew / relatedRowDelete serve the line-items add and remove, asked of schema.childObject.
    • Residual: the panel hands objectSchema: null although it holds childSchema, whereas RelatedRecordActionsBridge hands childDef for the same rows. So the managed-object policy leg is not read on line items. The client asks less than the server, never more. This is the family's p2 shape at narrower reach, not a wrong grant, and it goes to the card's remainder.
  • cardsMovable is a prop, not a published surface.
    • It is declared on KanbanBoardCoreProps and KanbanBoardProps, with default true.
    • plugin-kanban's only entry re-exports neither module nor either props type, and KanbanRendererProps is unchanged.
    • It is not a schema key, and no accept set changes.
  • Fail-open — as the map defines it. Every site reads resolveAffordance, and each runtime pin carries a no-provider leg.
  • The census against triage 6095316139.
    • A write call site with no WRITE_SITES entry fails the both-ways equality and is named, which matches the ruling's "a write affordance added later without a row turns it red". The dev's ablation A3 showed that red.
    • A site entered as unmapped(why) stays green, so the backlog is stated, not frozen. The ruling's first sentence is not yet true for the 21. The remainder's first act is to freeze the 21 keys, so that a new unmapped entry is red.
    • The stated limits (raw-HTTP writes, and affordances that only lead to a write) are named in the pin header.
    • The PR's first line is Part of #12082, with no closing keyword anywhere. That is consistent with the card staying open for its remainder, per triage's "no successor card".
  • Accept set: no schema key is added, removed or re-typed in any of the five packages. The docs describe what the diff lands.

② Semver level

  • @object-ui/core minor, RIGHT: AFFORDANCE_GRANTS reaches the entry through export *, and the value, the ConsoleAffordance union and resolveAffordance's accepted literals all widen. Clause-②: yes is well-formed.
  • plugin-view, plugin-calendar, plugin-kanban and plugin-form patch, RIGHT: behaviour only.
  • No major, and the changeset gates are green.

③ Boundary flags

  • The open question on the 21 unmapped sites: answered C by the seat (PR comment 6098064415). Escalated: the card needs a claim amendment that carries the answer, the file-surface additions and the backlog as the written remainder, before landing. The B leg's grading is triage's.
  • Files beyond the claim's surface (KanbanBoardCore.tsx, KanbanImpl.tsx, four plugin .mdx files, the core README, the changeset): right. The board draws the drag affordance, and the docs and changeset are owed.
  • The TypeScript parser instead of the shared comment mask: right. typescript is already a devDependency, and the relevant checks are green.
  • Trailers: model-free, and there is no closing keyword anywhere.
  • NOT MEASURED locally, answered by the head's runs: Bundle Analysis green (3169.6 of 3204.6 KB), README Export Check green, Test shards 1–8 green.
  • Out-of-scope findings (ExcelImportBar, the field-level question on the update rows, the line-items cell limits) are noted. Two items from ① go to the card's remainder: handing childSchema on line items, and freezing the backlog.
  • The red check, Spec Main Shape Gate (run 114223170489), is not this diff's.
    • Its ten annotations are the five test files objectui#12093 anchors. None of them is in this PR.
    • Each is blob-identical between the head and the merge-base de302c73.
    • The other 42 runs: 39 success, 3 skipped by design.

Implemented-by: claude/issue-12082-write-affordance-census
Reviewed-by: session_01B1gHb9baeX7oioD5sHVm7z

VERDICT: PASS


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3169.4 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-BEj-BFx-.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) 586.66KB 141.23KB
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.66KB 15.64KB
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.59KB 46.00KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 53.23KB 16.71KB
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) 92.11KB 23.27KB
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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 04ceb0ef78570d56f7f5ab6848c897b48af79119
Local-runs: none

① Derived judgments

The hop from the reviewed head — a base merge, no change to the diff. 04ceb0ef7 is one merge commit (parents d18c611d2, the head reviewed in 6098259561, and ce991bd70, which is origin/main); main moved by exactly one commit since de302c73: #12100 (objectui#12085 and #12093, the Spec Main Shape Gate fix). Measured: the hop's 22-file list equals #12100's file list; every one of those 22 is blob-identical to origin/main except packages/plugin-view/src/ObjectView.tsx; the PR's 18 files are blob-identical across the hop except that same file; the PR's net patch against its base (ce991bd7..04ceb0ef versus de302c73..d18c611d2) is identical hunk body for hunk body, 1606 lines each, with one @@ offset moved by the 16 net lines #12100 added above renderToolbar. Net diff stat unchanged: 18 files, +1270/−38. The merge commit's message carries no model identifier and no closing keyword.

The merged ObjectView.tsx — both sides' intent kept. #12100's region: AuthoredFilterSegments.tableDefaults re-typed to ObjectGridSchema['filter'], the new legacyTableDefaults(table) back-compat read of the retired table.defaultFilters, and the one call site tableDefaults: legacyTableDefaults(schema.table). This PR's region: the two import bindings (resolveAffordance, type SchemaLike) and renderToolbar's createGranted ANDed onto showCreate / operations.create. The regions are disjoint, neither reads the other's symbols, and the PR-side hunk is byte-identical to the reviewed head's. Type Check, Lint and the Test shards are green on the merged file.

Does this PR's census or any pin read #12100's region — no. The write census walks the file for DataSource write members at the interface's arity, WRITE_HELPERS calls and resolveAffordance / resolveFieldAffordance / formFieldsAffordance reads; #12100's additions contain none (a plain function reading a schema key, and a type). The ledger's one entry for the file (ObjectView > onClick :: delete, mapped rowDelete, read by ObjectGrid.tsx) and the listNew rows-read assertion are unchanged. ObjectView.createGrant-12082 authors no table, defaultFilters or filter fixture. The census and the four runtime pins read exactly what they read on d18c611d2.

Carried forward unchanged from 6098259561 (the code it judged is blob-identical at this head):

  • The three new rows, each right against the write it gates: calendarQuickCreate { crud: 'create', grant: 'create' } on ObjectCalendar > submitQuickCreate :: create, a closed row withholding onDateClick and onTimeRangeSelect from CalendarView, which draws both from handler presence; calendarReschedule { crud: 'edit', grant: 'update' } on handleEventDropDefault :: update, a closed row withholding onEventDrop, from which CalendarView sets draggable, the resize handles and the early return; kanbanCardMove { crud: 'edit', grant: 'update' } on ObjectKanban > persistCardMove :: update, a closed row handing the board no onCardMove and drawing the cards not movable (useSortable disabled through the KanbanCardsMovable context, aria-disabled), with KanbanBoardInner writing only inside if (onCardMove). The grant and operation set are asked of schema.objectName, the object each handler writes; the calendar's policy half reads the loaded schema only when it IS that object. A host-supplied onDateClick / onEventDrop is the host's channel, passed through and pinned. The rows have the shape of rowEdit / recordEdit (object-level, no field): the client asks the object question, never a rule the server does not have.
  • The reuses, right: listNew for the object-view New (the console list page's own row, with objectSchema from useSettledSchema; the opener is pinned by its runtime test, its form's submit sits behind createFormFields); relatedNew / relatedRowDelete for the line-items add and remove, asked of schema.childObject, ANDed onto schema.readonly, grant and CRUD bits matching the two child writes through LineItemsPanel > save :: batch. Residual, recorded on the card by 6098266400: the panel hands objectSchema: null while holding childSchema, so the managed-object policy leg is not read where RelatedRecordActionsBridge reads it; the client asks less than the server, never more.
  • cardsMovable: a prop on KanbanBoardCoreProps and KanbanBoardProps, default true; packages/plugin-kanban/src/index.tsx (the only entry; exports is . and ./style.css) re-exports neither module nor either type; KanbanRendererProps unchanged; nothing reads schema.cardsMovable. Not a published surface, not a schema key, no accept-set change.
  • Fail-open as the map defines it: every site reads resolveAffordance, whose grant leg reads open with no provider; each of the four runtime pins carries a no-provider leg; fieldWriteGate.ts untouched.
  • The census against triage 6095316139: a write site with no WRITE_SITES entry is red, naming it (the both-ways equality; ablation A3 showed it); a site entered as unmapped(why) is green and the unmapped key set is not frozen, so the backlog can grow by one reviewable ledger line. The limit is closed for an un-entered site and stated for an entered one; the freeze is recorded for the remainder in 6098266400. The 21 unmapped entries, keyed by symbol with reasons and candidate rows, are the card's remainder: Part of #12082 is the PR's first line, with no closing keyword in the body or in any commit, the merge included.
  • Accept set: no schema key added, removed or re-typed in any of the five packages; the component props types are unchanged. Docs (four plugin mdx, core README, the map's docblock) describe the landed behaviour; README Export Check and Internal Docs Link Check green on this head.

② Semver level

Unchanged from 6098259561; the changeset is blob-identical across the hop, and every sentence re-reads true against the code at this head (the code it names is blob-identical, and the object-view New sentence still describes renderToolbar's createGranted ANDed onto the node's two toggles in the merged file).

  • @object-ui/core minor — right. AFFORDANCE_GRANTS reaches the entry through export * from './utils/affordanceGrants.js'; it gains calendarQuickCreate, calendarReschedule and kanbanCardMove, widening the published value, the ConsoleAffordance union and the literal set resolveAffordance accepts; nothing removed or renamed. Clause-②: yes at line start of the PR body, Clause-②: yes (widening) in the changeset body: yes takes at least minor, one arm, well-formed.
  • @object-ui/plugin-view, plugin-calendar, plugin-kanban, plugin-form patch — right: behaviour only, no published type, export or schema key moves.
  • No major; Changeset Bump Policy, Changeset Declaration, Changeset Claim Re-read, Changeset Fixed Group Check and Changeset Overwrite Report green on this head. No model identifier and no line address in the changeset.

③ Boundary flags

  • Check-runs on this head: 43 runs, 40 success, 3 skipped (the expected dependabot, Test (coverage), Test (coverage shard)), 0 failure. Spec Main Shape Gate (run 114294264966) is green: the five files it named on d18c611d2 now carry fix(components): element:repeater and element:number publish the dataSource binding as their only query input, and objectui compiles against spec main again (objectui#12085, objectui#12093) #12100's fix and are blob-identical to origin/main at this head; none is in the PR's diff. mergeable_state clean. Console Performance Budget PASS (3169.4 KB of 3204.6 KB). Every landing condition the earlier record left to the seat is met on this head.
  • Open question (the 21 unmapped sites) — answered: C, recorded on the card by the seat's ACCEPT and claim amendment 6098266400 (A, next for this card: the record-overlay inline edit at RecordDetailPanel, the gantt drag-resize / dependency edit / row Delete, the grid bulk field update; B, recorded until reach is measured: Setup assignment, the designer Save, the sys_comment / sys_comment_reaction feed writes, the related list's Add existing / fallback remove). The escalation in 6098259561 is closed by that comment. At landing the seat releases the claim to pm:queue + pm:retriage with that remainder written on the card; the grading of the B leg is triage's.
  • Files beyond the claim's surface (KanbanBoardCore.tsx, KanbanImpl.tsx, four plugin mdx, core README, the changeset) — right, and now named in the claim amendment 6098266400.
  • From the earlier record, recorded for the remainder in 6098266400: freeze the 21 backlog keys so a new unmapped entry is red until graded; hand the line-items panel's childSchema to the affordance source as the related list does; the ExcelImportBar mount of ImportWizard reads no grant (one site key, two mount paths, the census's stated limit).
  • TypeScript parser instead of the shared comment mask — right: comments are trivia to the parser; typescript is already plugin-form's devDependency; Pre-Install Import Graph Check and the Test shards (carrying check-hand-rolled-comment-mask) green on this head.
  • Trailers and keywords — the four non-merge commits carry the model-free pair only; the merge commit carries a plain merge subject; no model identifier in the PR body, changeset, docs or code; no closing keyword anywhere, consistent with Part of.
  • Process notes (lock holds, the refused first A3 attempt, the 429 interruption with re-taken readings, the worktree removal) — no contract effect; this head's check-runs are the readings of record.
  • NOT MEASURED locally, answered by this head's runs: check:eager-closure → Bundle Analysis green and the budget PASS; check:readme-exports → README Export Check green; the full scripts/__tests__ → Test shards 1–8 green.
  • Out-of-scope findings — the field-level question for the two update rows (the server's refusal; the rows' shape matches rowEdit), the line-items cells under a create-only child grant and the whole-grid remove gate (pre-existing GridField limits): noted, on the card, not this PR's.

Implemented-by: claude/issue-12082-write-affordance-census
Reviewed-by: session_01B1gHb9baeX7oioD5sHVm7z

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants