Skip to content

fix(core,app-shell,plugin-detail): freeze the write census's backlog; the record feed and a related list's Add existing / fallback remove read the affordance-to-grant map (objectui#12082) - #12140

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-12082-census-freeze-leg-b
Oct 11, 2026

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #12082 (the census freeze and leg B's non-Setup sites; the four Setup entries stay on the card)
Clause-②: yes

Claim 6108058067 (seat domain:ui#2), triage 6102063887. Implemented by an os-dev run dispatched from session https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD.

Clause-② is yes, not the claim's predicted no: four rows join AFFORDANCE_GRANTS, a published value of @object-ui/core, so core is minor. The contract review at CONTRACT_REVIEW_TIER is owed.

What lands, in triage's order

1. The backlog freeze, and leg A's HOST_GATED limit closed

  • UNMAPPED_BACKLOG (the census pin affordanceGrantMap-12082.test.tsx) is a closed list of the keys WRITE_SITES may grade unmapped, held equal to the ledger both ways. A NEW unmapped entry is red until graded; a key graded out of unmapped must leave the list. It was frozen at the 13 keys on BASE, and each later commit deletes the keys it grades. At this head it holds the four Setup assignment keys only.
  • The hand-written HOST_GATED list is replaced by a derivation, hostGatedSites: a mapped site is host-gated when a file it names in readBy JSX-mounts the component the write is written in, and then every mount must be in readBy. Measured on this tree it finds exactly the site the list held (the import wizard). So a NEW host-gated component needs no list entry to be seen. What it cannot see is written beside it: a host that reaches the write through a hook, a function, an alias or a wrapper component, not a JSX element of the component's own name.

2. Leg B: the nine non-Setup sites, reach measured per site

Site Reading Outcome
record feed: handleAddComment, handleAddReply (create sys_comment) reachable: the composer is drawn for every caller, and the everyone baseline grants no sys_comment bit (an app grants it). Read on objectstack main 67b669e6: member_default names no sys_comment; viewer_readonly has '*' read with no create; the showcase app's sets name sys_comment zero times; the security middleware's step "2. CRUD permission check" maps insert to allowCreate and refuses with 403 PERMISSION_DENIED mapped to a new row feedComment (create)
record feed: handleToggleReaction > write create / delete (sys_comment_reaction) reachable: the same reading; the reaction object's own docblock and its floor pin say its create / read / delete bits come from an app-shipped set, never the baseline mapped to new rows feedReactionAdd (create) and feedReactionRemove (delete)
record feed: handleToggleReaction update (sys_comment column store) reachable on a deployment with no sys_comment_reaction mapped to a new row feedReactionColumn (update)
dashboard designer: saveSchema (update sys_dashboard) the target does not exist: sys_dashboard is a per-type table objectui retired (dashboards persist through /meta, ADR-0034, the header of runtime-metadata-persistence.ts), and objectstack main registers no such object (absent from the spec's PLATFORM_OBJECTS_BY_PACKAGE and CLOUD_PROVIDED_OBJECT_NAMES; the repo tree has no dashboard object file, control sys-comment-reaction.object.ts 1 hit). The Save fails for every caller whatever the grant, and no in-app link reaches design/dashboard/NAME outOfFamily, with that reading
related list: handleAddRecords create (linkField set) reachable: the Add button showed for every caller. Measured where the write goes: a link row of the list's OWN object (the junction) mapped to relatedNew (reused: create on the list's object)
related list: handleAddRecords update (no linkField) measured: re-parents the PICKED record (add.picker.object) mapped to relatedRowEdit (reused: update on that object)
record:related_list: the fallback remove reachable, and worse than "reads no grant": on a record page the host bridge withholds onDelete exactly when relatedRowDelete is closed, and the fallback (an add config, no host delete) then put the delete straight back mapped to relatedRowDelete (reused), read with the list object's registered schema, so it never re-opens what the host closed

H4's two candidates were both right, one per branch: the junction branch is relatedNew on the junction, the re-parent branch relatedRowEdit on the picked object.

The record feed, how it reads the rows

RecordDetailView hands each handler to the discussion context only where its row allows it, and the panel draws the composer, the reply input and the reaction picker from the handler's presence. Each row is asked of the object it writes, with that object's schema from the registry, so policy and the effective operation set are read too. On the records store the control is the add row; a take-back click asks the remove row and, closed, writes nothing (no write the server would refuse, and nothing shown changed). With no permission provider every grant reads open, as before.

Files

  • packages/core/src/utils/affordanceGrants.ts: four rows.
  • packages/app-shell/src/views/RecordDetailView.tsx: resolveFeedWriteGates and the handler hand-over.
  • packages/plugin-detail/src/RelatedList.tsx: addGranted gates the Add button and its dialog.
  • packages/plugin-detail/src/renderers/record-related-list.tsx: removeFallbackGranted.
  • The census pin; three new runtime pins; the changeset; packages/core/README.md, packages/plugin-detail/README.md, content/docs/plugins/plugin-detail.mdx (objectui AGENTS.md rule 2).
  • Not edited: AssignedUsersSection.tsx, PositionHoldersSection.tsx and their entries (deferred to the objectui#7611 epic's C9), and DashboardDesignPage.tsx (graded, not changed).

Evidence

  • Measured on BASE first (c9fc10b1, restore-trapped): the four source files checked out at BASE (4/4 blobs equal BASE on disk, each fix anchor grep -c 0), the HEAD pins and census run against them: Test Files 4 failed (4), Tests 20 failed | 64 passed (84). Red per pin, every red in the defect direction (an affordance offered under a grant that does not hold it): feed 9 of 18, Add existing 5 of 9, fallback remove 4 of 8, census 2 of 49 (the row table and the mapped-site check). Controls stayed green (no provider; the full shape; the host's own delete). Restore: 4/4 blobs equal HEAD, git diff HEAD empty, status clean.
  • Ablations, objectstack scripts/ablation-replace.mjs WRAP mode, predictions written first, each restore blob == HEAD and git diff HEAD empty:
    • A1 grade an existing mapped entry unmapped: census 1 failed / 48 passed, the freeze test (added). As predicted.
    • A2 put a graded key back into UNMAPPED_BACKLOG: census 1 failed / 48 passed, the freeze test (left). As predicted.
    • A3 plant a JSX mount of ImportWizard in a file that reads no listImport: census 1 failed / 48 passed, the host-gated mount test. As predicted.
    • A4 hand onAddComment over unconditionally: feed pin 4 failed / 14 passed (the four closed composer shapes); the census stays green, since the file still reads feedComment. As predicted: the runtime pin catches what the census cannot.
    • A5 the junction Add reads relatedRowEdit: 3 failed (census: no file it names reads relatedNew; Add pin: the create-only and edit-only junction shapes). As predicted.
  • Suites and gates: the readings at the final head are in the os-dev-report comment on the card. At open: pins + census green on the main merge (84 passed); all 48 RecordDetailView* files 312 passed; plugin-detail shard 1/4 713 passed | 8 skipped; type-check of core, plugin-detail, plugin-form exit 0, each echoing its own script, 0 error TS; eslint on the 8 touched .ts/.tsx 0 errors, and each modified source file's warning count equal to BASE.

Acceptance notes

  • The reply composer cannot be reached from a read row. RecordActivityTimeline draws ThreadedReplies (and the reply input) only when replyCount is above 0, and nothing derives replyCount on read: the record page sets it only after a reply is posted in the session. sys_comment's own docblock says the count is taken from parent_id children at read time. So a reply read back from the server is not shown, and the reply input appears on no read row. The reply site is still mapped to feedComment (same row and gate as the composer). Noted, not filed: no reach measured at a door.
  • A partial reaction grant. A caller who can create but not delete their reaction row gets the control, and a take-back click writes nothing. A caller who can delete but not create gets no control, so cannot take back an older reaction. Both follow from one control that both adds and takes back.
  • The dashboard designer's Save is a dead write. Graded outOfFamily with the reading above. The route has no in-app entry (the dashboard view's top-bar "Design in Studio" deep-links to Studio's own surface), so it is reached by URL or the recent list only, and a Save there fails silently (saveSchema swallows the error). Noted, not filed.
  • Object verdict only, as on leg A: no per-record author check (the server's own comment gate also asks author-or-parent-editor of an update, and the column store's reaction update is one).
  • The feed reads sys_comment / sys_comment_reaction schemas from the page's objects; absent, the default bucket is read, so the grant and the operation set still decide.

Gate lead (hand-derived, objectui has no dispatch-gates)

Build closure turbo run build for app-shell^..., plugin-detail^..., plugin-form^...; type-check for core, app-shell, plugin-detail, plugin-form; vitest for those packages (sharded), the census and every -12082 pin; the two repo ratchets and scripts/__tests__/; the changeset, citation, control-byte, test-path, vi-mock, phantom-deps and doc gates.


Generated by Claude Code

…ive its host-gated sites (objectui#12082)

The census's 13 `unmapped` WRITE_SITES entries are now a closed list,
UNMAPPED_BACKLOG, held equal to the ledger both ways: a NEW `unmapped`
entry turns the census red until the site is graded, and a key graded
out of the backlog must leave the list, so the backlog only shrinks.

Leg A's hand-written HOST_GATED list is replaced by a derivation: a
mapped site is host-gated when a file it names as a reader mounts the
component the write is written in, and then every mount must be a file
the site names. On this tree it finds the one site the list held (the
import wizard); what it cannot see (a host that reaches the write
through a hook, a function, an alias or a wrapper) is stated beside it.

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

Leg B of the write census, the feed's five sites. The record page handed
the feed's comment composer and reaction control their handlers
unconditionally, so a member whose sets grant neither sys_comment nor
sys_comment_reaction (the everyone baseline names neither; an
application grants them) was offered a post the server refused at its
object CRUD step.

Four rows join AFFORDANCE_GRANTS, each asked of the object it writes:
feedComment (create on sys_comment: the composer and the reply
composer), feedReactionAdd and feedReactionRemove (create and delete on
sys_comment_reaction: the reaction control and its take-back), and
feedReactionColumn (update on sys_comment: the reaction control on a
deployment with no sys_comment_reaction). The record page hands each
handler over only where its row allows it; a take-back click with the
remove row closed writes nothing. No provider: every grant reads open.

The five census entries move to mapped and leave the frozen backlog.

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

Leg B of the write census, the related list's three sites. Add existing
(the `add` picker) read no grant. Measured where its write goes, it is
one of two, each a row the map already has: with a `linkField` it
creates a link row of the list's own object (relatedNew, create on the
list object); without one it re-parents the picked record
(relatedRowEdit, update on the picked object). RelatedList withholds
the button and its dialog where that row is closed.

The fallback remove (an `add` config, no host delete) deleted with no
grant read, and on a record page it put back exactly the delete the
host bridge had withheld for a closed relatedRowDelete. It now reads
relatedRowDelete of the list object, with the object's registered
schema, so it can never re-open what the host closed. No host and no
permission provider: still offered, as before.

The three census entries move to mapped and leave the frozen backlog.

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

Leg B's dashboard designer site. Its Save writes `sys_dashboard`, a
per-type table objectui retired (dashboards persist through /meta,
ADR-0034) and one no objectstack server registers: it is absent from
the spec's platform object registry, OSS and cloud alike. So the write
fails for every caller whatever their grant, and no row can make the
affordance honest; no in-app link reaches the route either. The entry
leaves the frozen backlog as outOfFamily with that reading; the four
Setup assignment entries are what remains.

Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
Co-authored-by: Claude <noreply@anthropic.com>
…sting and fallback remove, and the frozen backlog (objectui#12082)

The core README names the four feed rows and the related list's reuse
of relatedNew / relatedRowEdit, and says the census backlog is frozen.
The plugin-detail README and docs page say which grant the record
feed's composer, reply input and reaction control need, and which row
a related list's Add existing and its fallback remove read. Changeset:
@object-ui/core minor (four rows widen the published AFFORDANCE_GRANTS),
@object-ui/app-shell and @object-ui/plugin-detail patch.

Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
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) 3175.3 KB 3204.6 KB
Main entry chunk (gzip) 74.1 KB 350 KB
Entry file index-DFXVC4NS.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) 20.05KB 7.41KB
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.00KB 141.35KB
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) 275.07KB 70.00KB
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) 15.35KB 5.51KB
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) 54.31KB 15.86KB
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) 250.21KB 66.02KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.80KB 46.04KB
plugin-gantt (index.js) 179.37KB 45.17KB
plugin-grid (index.js) 255.15KB 70.92KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.77KB 31.19KB
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.21KB 11.85KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3175.3 KB 3204.6 KB
Main entry chunk (gzip) 74.1 KB 350 KB
Entry file index-DFXVC4NS.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) 20.05KB 7.41KB
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.00KB 141.35KB
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) 275.07KB 70.00KB
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) 15.35KB 5.51KB
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) 54.31KB 15.86KB
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) 250.21KB 66.02KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.80KB 46.04KB
plugin-gantt (index.js) 179.37KB 45.17KB
plugin-grid (index.js) 255.15KB 70.92KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.77KB 31.19KB
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.21KB 11.85KB
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: 6dea96f4536e38870d93595d6c05b026cabba4aa
Local-runs: none

Inputs: card objectui#12082 (body and all 20 comments, the dev's report 6109609151 on this round, the claim 6108058067, triage 6102063887, the leg-A record and ACCEPT), PR objectui#12140 (body, 12-file list, the net diff against main at the head), the 43 check-runs on the head, and the files the diff reads at the head and at the merge base, plus the objectstack side at main 31b5a5f7 (the security middleware's CRUD step, permission-evaluator's operation map, comment-access-hooks, the two feed object definitions, the shipped default permission sets, the spec's CRUD_AFFORDANCE_DEFAULTS and platform-object-names). Read at 2026-10-11T13:52Z. Rendered by an isolated reviewer adopted by the seat session named below.

Shape: draft on main, head repo equals base repo, 908 added / 51 removed over 12 files, first body line Part of #12082 (...), Clause-②: yes at a line start, no closing keyword. No governed path on the file list. Check-runs on the head: 43 read, all completed: 39 success, 4 skipped by design (Test (coverage shard ...), Test (coverage), dependabot), 0 failure, 0 in_progress. Green include Lint, Type Check, Test (shard 1/8) through Test (shard 8/8), Test (dist pins), Build & E2E, Governed Surface Queue Guard, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read, README Export Check, Spec Main Shape Gate, Line Citation Gate, Inert vi.mock Specifier Check, Control Byte Scan, Bundle Analysis.

① Derived judgments

  1. @object-ui/core publishes four new rows of AFFORDANCE_GRANTS (21 rows at base, 25 at head; a name-by-name diff shows four additions and no edited or removed row): feedComment (crud create, grant create), feedReactionAdd (crud create, grant create), feedReactionRemove (crud delete, grant delete), feedReactionColumn (crud edit, grant update). ConsoleAffordance widens by the same four names; resolveAffordance accepts them. The module is re-exported from src/index.ts on the one . entry of package.json exports, so this is a widening of a published value and type, purely additive. RIGHT.

    • The grants against the server. On objectstack main, OPERATION_TO_PERMISSION maps insert to allowCreate, update to allowEdit, delete to allowDelete; the security plugin's step "2. CRUD permission check" asks checkObjectPermission for every object with no sys_comment carve-out (the ADR-0056 D2 deny baseline: an empty set grants nothing, PERMISSION_DENIED). comment-access-hooks ADDS a parent-read requirement on insert and an author-or-parent-editor rule on update and delete; it replaces nothing. So the object grant each row names is a necessary condition of the write, which is what a map row states. RIGHT.
    • The crud bits against policy. Both feed objects are managedBy: 'platform', and the spec's CRUD_AFFORDANCE_DEFAULTS.platform is open on create, import, edit and delete. sys_comment_reaction declares userActions: { edit: false, import: false } and apiMethods: ['get', 'list', 'create', 'delete', 'bulk'], so its create and delete bits stay open and only edit closes, which no row asks of that object. sys_comment declares no userActions and no apiMethods. In the console, where RecordDetailView is mounted with objects={allObjects} from useMetadata() and both schemas are present, the policy leg is therefore open and the caller's grant plus the server's effective operation set decide. The rows do not close the feed for everyone. RIGHT.
    • The baseline reading. default-permission-sets.ts on main names sys_comment nowhere, and member_default carries the comment "[The three objectui stream labels ruled on 2026-08-21 were never created as label objects — every queue card in this repo is unroutable, and the spec/devx lanes read this repo as empty #5491] NO '*' WILDCARD GRANT"; the hooks' header sentence about wildcard CRUD is stale prose, as the dev says. So an application set is what grants either object, and the defect (a composer offered to a member whose sets name neither object) is real. The dev's reading holds on today's main.
  2. @object-ui/app-shell, RecordDetailView. resolveFeedWriteGates is module-private (no export, no prop change). It resolves the four rows with each object's schema found in the objects prop (absent reads as the default bucket, which the spec also treats as platform) and perms = usePermissions(), and the record page hands onAddComment and onAddReply over only on feedComment, onToggleReaction only on feedReactionAdd (records store) or feedReactionColumn (column store). RecordActivityTimeline draws the composer from showCommentInput = ... && !!onAddComment, the reply input from showReplyInput={!!onAddReply} and the picker from onToggleReaction presence, so a closed row withholds the affordance rather than greying it after the fact. A take-back on the records store returns before any write when feedReactionRemove is closed (if (!want && !reactionRemoveGranted) return;), with reactionRemoveGranted in the callback's dependency list. While the registry has not settled (reactionStore === null) the handler is now undefined where it used to be handed over; the feed read waits on the same settle (if (feedsEnabled && reactionStore === null) return;), so no comment is on screen in that window and nothing visible changes. With no provider, can answers true and getObjectApiOperations undefined, so every gate reads open, as the control case pins. RIGHT.

  3. @object-ui/plugin-detail, RelatedList Add existing. handleAddRecords measured at the head: with add.linkField it calls dataSource.create(api, ...), a row of the list's OWN object; without one it calls dataSource.update(add.picker.object, id, ...), a re-parent of the picked record. addGranted reads relatedNew on api with the list's settled schema for the first, and relatedRowEdit on add.picker.object for the second, and both the button and RecordPickerDialog sit behind it. The rows are the ones a related list's "+ New" and row Edit already read, so the verb-on-object pairing is exact. No prop or export changes. RIGHT. One limit, carried to ③ (d): on the re-parent branch the schema handed to the resolver is the list's own only when the picked object IS the list's object, otherwise null; the policy leg then reads the default bucket while the grant and the effective operation set are still read.

  4. @object-ui/plugin-detail, the record:related_list fallback remove. removeFallbackCandidate (an add config, a data source, no host onDelete) now triggers useMetadataItem('object', objectName), and removeFallbackGranted reads relatedRowDelete with the registered schema, withheld while the lookup is in flight. The host's own onDelete path is untouched. On the record page RelatedRecordActionsBridge sets handlers.onDelete only when resolveAffordance('relatedRowDelete', source).allowed, so the fallback can no longer re-open a delete the host withheld, which the new pin asserts. Without a metadata provider useMetadata() returns the no-op context whose getItem answers null, so the lookup settles loading: false, item: null and the fallback reads the default bucket plus the grant, the context-free embed the README promises (control pinned). The widened lookup trigger changes no action placement: relatedListActionsNeedLookup is true only for authored string ids, and placeAuthoredRelatedListActions consults registered for ids alone; the only text that can differ is the refusal sentence's "object registered" flag in a malformed-actions edge, and it differs toward the more accurate wording. RIGHT.

  5. The census pin (plugin-form, test file only). UNMAPPED_BACKLOG is a closed set held equal to the ledger's unmapped keys in both directions (the added / left test); at the head the ledger carries exactly four unmapped( entries, the four Setup assignment keys, and the set lists those four. The hand-written HOST_GATED is replaced by hostGatedSites, derived from the ledger and the tree (a mapped site whose readBy file JSX-mounts the component the write is written in), with the mount walk now recording every JSX element name; the control asserts the derivation finds ImportWizard and reaches both its mounts, and the blind spot (a host reaching the write through a hook, a function, an alias or a wrapper) is written beside the derivation rather than implied away. EXPECTED_GRANT holds 25 rows, equal to the map. The dashboard designer's saveSchema is graded outOfFamily on a measured reason that holds: it calls dataSource.update('sys_dashboard', ...) inside a try whose catch swallows, and sys_dashboard is absent from both PLATFORM_OBJECTS_BY_PACKAGE (whose plugin-audit row is sys_activity, sys_audit_log, sys_comment, sys_comment_reaction) and CLOUD_PROVIDED_OBJECT_NAMES on objectstack main. The nine non-Setup leg-B keys are graded (eight mapped, one outOfFamily), as triage 6102063887 asked. RIGHT.

  6. Docs. packages/core/README.md, packages/plugin-detail/README.md and content/docs/plugins/plugin-detail.mdx name the four rows, their grants and their objects exactly as the map does, and the related-list paragraph names relatedNew / relatedRowEdit / relatedRowDelete for the writes measured above. README Export Check is green. RIGHT.

No accept-set is narrowed and no published signature changes anywhere in the diff.

② Semver level

  • .changeset/12082-census-freeze-leg-b.md: @object-ui/core: minor, @object-ui/app-shell: patch, @object-ui/plugin-detail: patch. Core widens a published as const value and its key union by four names, additive, so minor is the floor and the ceiling under objectui's convention (never major; Changeset Bump Policy green). app-shell and plugin-detail move behaviour only, with no export, prop or type change, so patch. plugin-form changes one test file under src/, which publishes nothing; the one changeset file satisfies the presence declaration for the whole change (Changeset Declaration green) and the packages version together as one fixed group in any case. The changeset prose, read sentence by sentence against the diff, names the rows, the two Add-existing branches, the fallback remove and the fail-open rule correctly, and carries no model name. MATCHES.
  • Clause-②: yes, bare, at a line start of the PR body. Right: a published value widens. yes takes at least minor, and core is minor. No arm is owed: nothing is removed or renamed, so there is no (narrowing), and a bare yes is a legal declaration. The claim 6108058067 predicted no; the measurement supersedes the prediction, and the claim line is the seat's to amend (③ item 1).

③ Boundary flags

The dev's report 6109609151 carries open_questions: []. Its deviations and findings, each answered or escalated:

  1. Clause-② is yes, not the claim's no. Answered: the PR body's line is right (② above). Escalated to the seat: amend claim 6108058067's Clause-② line to yes in the ACCEPT, as the leg-A ACCEPT did; this record is the review the claim said that correction would owe.
  2. PR body predates the final readings (the "Suites and gates ... At open:" sentence; the second merge of main, ce9811c, is not in the body). Escalated to the seat as a body amendment; prose, not contract, and the head's check-runs are the readings of record.
  3. H3 did not hold; the designer Save is outOfFamily for a different measured reason. Answered: verified on objectstack main (① item 5). Right.
  4. Suites narrowed locally (core to the map's readers, plugin-form to the census, app-shell to the RecordDetailView* files; type-checks and plugin-detail shards at an earlier head). Answered by the head: every test shard, Test (dist pins), Type Check and Build & E2E are success on 6dea96f.
  5. A temporary probe line in the census, never committed. Answered: the diff carries no scratch write.
  6. git fetch origin main ran twice in the worktree (a write to the shared refs/remotes/origin/main). Accepted as declared: the dispatch's own setup makes the same write, and it has no effect on the diff or the head.
  7. check:nul-bytes and check:doc-links are not objectui scripts. Answered: Control Byte Scan and Internal Docs Link Check are green on the head.
  8. Comment count 19 versus the dispatch's 20; model-free trailers; worktree removed. Noted; nothing owed.
  9. Finding: the reply composer cannot be reached from a read row, and a reply read back is not shown. Verified at the head: RecordActivityTimeline draws ThreadedReplies (and so the reply input) only when enableThreading holds and the item's replyCount is above zero, and the spec's sys_comment docblock prescribes counting parent_id children at read time, which nothing on the page does. The mapping here (feedComment for the reply site) is right regardless. ESCALATED to the seat: this is a user-visible defect candidate of its own (a reply posted in another session is invisible on reload), filed on the source-read evidence above or recorded with the reason it stays a note. Not a condition on this PR.
  10. Finding: the dashboard designer route is an orphan with a dead, silently failing Save. ESCALATED to the seat: a surface that advertises a Save the runtime cannot deliver is the trim-or-file case; a separate card for DashboardDesignPage's sys_dashboard write (or the route). Not a condition on this PR.
  11. Finding: comment-access-hooks.ts's header prose on objectstack still says the default sets grant wildcard CRUD. Verified stale on main. ESCALATED as a one-line docs note for the objectstack lane.
  12. PR Acceptance notes carried as declared: a partial reaction grant (delete without create gets no control; create without delete gets a control whose take-back writes nothing) follows from one control doing both; the object verdict only (no per-record author or parent-editor check, the column store's reaction update included) is the posture of every row of the map; schemas absent from the page's objects read the default bucket. Accepted.

Reviewer-found, carried as notes, not conditions:

  • (d) The re-parent branch of Add existing reads the policy leg from the default bucket whenever the picked object is not the list's own object; the picker's schema is fetched only once the dialog opens, so it is not handed to the resolver. The grant and the effective operation set ARE read, so the server's refusal is matched on both; a userActions.edit: false on the picked object alone is not. Same class as leg A's declared default-bucket reads. The seat adds it to the PR's Acceptance notes.
  • (e) The feed pin's objects prop omits both feed objects, so the policy leg against the registered sys_comment and sys_comment_reaction schemas is judged in this record from the spec's CRUD_AFFORDANCE_DEFAULTS and the reaction object's userActions and apiMethods, not exercised by a pin. One case with both registered schemas in objects would pin it; for the next round on this card.

The four Setup entries stay unmapped by the claim's deferral (the objectui#7611 epic's C9), so the card stays open and the PR says Part of #12082, as the claim and triage require.

Implemented-by: claude/issue-12082-census-freeze-leg-b
Reviewed-by: session_01TYgwmFK1q4KJ6Qq2WRLzsD

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 13:56
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 13:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 158c75d Oct 11, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12082-census-freeze-leg-b branch October 11, 2026 14:14
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