You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
console (17.7.0): a create form disables every field for a user whose grant is allowCreate without allowEdit — a create-only audience cannot fill the form it is allowed to submit #12082
Filing class: ① product defect (user-visible) — reach: browser, measured on @objectstack/* 17.7.0 (Console as pinned in 17.7.0). Maintainer instruction for platform problems found in testing, verbatim, 2026-10-10: 「你遇到的平台问题应该提交issue」.
Reader: objectui triage → the form renderer (plugin-form) / the Console's permission check.
Symptom
Measured in objectstack-ai/hotclm while fixing objectstack-ai/hotclm#90 (draft PR objectstack-ai/hotclm#91; report on #90). The app's intake screen flow embeds an object form in create mode for clm_contract_version. The requester's permission set grants that object allowCreate: true, allowEdit: false (the app's design: requesters create versions, never edit them). Once GET /api/v1/auth/me/permissions has loaded:
every field of the create form renders disabled — "You do not have edit access to this field.";
Save & Continue then posts an empty body: POST /api/v1/data/clm_contract_version → 400 VALIDATION_FAILED "Contract is required; Version No. is required; File is required".
So every user of a create-only grant is blocked at that step. (The server accepts the create: with the client's permission copy patched to allowEdit: true, the same create lands 201 under the user's real allowCreate.)
Cause (dev's source read of the 17.7.0 bundle — for triage to confirm)
checkField(object, field, 'write') reads objects[o].allowEdit, and plugin-form disables every field failing it whenever mode !== 'view' — create included. A create form should check allowCreate (field-level create/insert permission), not allowEdit.
Minimal repro
Any permission set with allowCreate: true, allowEdit: false on an object; open that object's create form (or a flow object-form screen in create mode) after /auth/me/permissions has loaded.
Dedupe
MCP search_issues on this repo, create form fields disabled when allowEdit false allowCreate true checkField write create mode → 2 hits: #11000 (closed — every field disabled when the create affordance is closed, the opposite grant) and #3402 (closed — unrelated). Neither covers a create-allowed, edit-denied grant.
Filed by the repo:hotclm PM seat from a measured dev finding.
Triage: first grade, bug · priority:p1 · domain:ui · area:access · pm:queue. Accepted as the close-out of the "affordance ≠ grant" family; #12081 items 1, 5 and 6 fold in here
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-10T03:53Z. ⛔ Not a claim, ⛔ not a dispatch.
Re-read on objectui main1b2d0160fe:plugin-form/src/fieldWriteGate.ts:138 disables a field whenever mode !== 'view' and checkField(object, field, 'write') fails, so create mode is included. The defect holds on main, not only in the 17.7.0 bundle.
Why p1: a role designed as create-only cannot create anything through the form. There is no workaround, and the server accepts the same create.
The family: each console affordance must read the grant it actually exercises. One pass of one app found four members:
this card: a create form gates its fields on edit;
objectui#12081 item 1: no Edit on the record header for a caller whose update is allowed;
item 5: New / Import shown to a caller with allowCreate: false;
item 6: a lookup picker offers "Create new" to a caller who cannot create the target object.
One map from each affordance to the grant it reads: create form fields → create; edit form fields → edit; header Edit → update; New / Import → create; lookup "Create new" → create on the target; and the rest the census finds. Every affordance reads that map. ⛔ No per-component permission logic.
Measure first, for create-mode fields: the server's field-level rule on insert. The field gate follows what the server enforces: a field the server would refuse on create stays disabled, and one it accepts is open. ⛔ No client rule the server does not have.
Enumeration pin: iterate every affordance in the map against four grant shapes: create-only, edit-only, read-only and full. Each affordance shows exactly when its grant allows it. An affordance added later without a map row turns it red.
Each item is measured on main first, since the report is from 17.7.0.
Claim: PM loop round 3
Session: session_01CGZy1BGCjdN5cXqL9cnvB8
Account: os-support-ai
Branch: claude/issue-12082-affordance-grant-map
Worktree: objectui-issue-12082
Domain: domain:ui
Seat: domain:ui#3
Scope: this card plus objectui#12081 items 1, 5 and 6, which triage folded in here (6093495104, 6093502833). Items 2, 3, 4, 7 and 8 stay on objectui#12081.
File surface (line numbers on main1b2d0160):
The map, one module: each console affordance → the grant it reads. Rows: create-form fields → create; edit-form fields → edit; record-header Edit → update; list New / Import → create; lookup "Create new" → create on the target object; plus every other row the census finds. Where it lives is the dev's measured choice, either beside resolveEffectiveCrudAffordances (packages/core/src/utils/managedBy.ts:177) or in packages/permissions/src/. ⛔ One copy, ⛔ no per-component permission logic.
Create-mode fields:packages/plugin-form/src/fieldWriteGate.tsgateByPermission (:138) asks checkField(…, 'write') whenever mode !== 'view'. A create form asks the create question instead. The resolver side lives in packages/permissions/src/ (MePermissionsProvider.tsxcheckField:302; PermissionProvider.tsx; the PermissionContext.ts type). The create answer follows what the server enforces on insert, measured first. ⛔ No client rule the server does not have.
any further reader the census finds, for example RelatedRecordActionsBridge.tsx:375, plugin-listListView.tsx:1883 and plugin-gridrowCrudAffordances.ts:238. Each is named in the PR.
Tests: the card's enumeration pin. Every row of the map runs against four grant shapes (create-only, edit-only, read-only, full), and each affordance shows exactly when its grant allows it. A reader that bypasses the map, or an affordance with no row, turns it red. Plus the card's repro as a form-level pin: a create-only grant gets enabled create-form fields.
One .changeset/12082-*.md per touched package, or one file naming all of them.
⛔ Not on it:
objectui#12081 items 2, 3, 4, 7 and 8;
any server, spec or /me/permissions envelope change (objectstack's lane);
the fail-open contract with no permission provider mounted (fieldWriteGate.ts docblock), which stays as is;
packages/components/src/ui/**.
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui over these paths: no path-derived mandate; default tier). The contract review runs at the ceiling, CONTRACT_REVIEW_TIER, because of the line below.
Clause-②: yes
Why Clause-②: yes: the map is a new exported surface read across packages (core or permissions, then app-shell, fields, plugin-form), and asking the create question may widen checkField's published action union. Changesets are minor where a published surface widens and patch where only behaviour moves. ⛔ Never major (objectui AGENTS.md §9).
Responsibility: objectui console: each affordance decides on its own which grant it reads, so a create form gates its fields on edit and a create-only role cannot create, the header hides Edit from a caller whose update is allowed, and New / Import / lookup "Create new" show to a caller who cannot create | the platform path: one affordance-to-grant map that every affordance reads, with create-mode fields following the server's insert rule | every user of a create-only, edit-only or read-only grant in any metadata app
Thread-read: 6093495104
Serial constraints cleared: none blocking.
In-flight claims, read 2026-10-10T04:23Z: objectui#6349 carries this seat's pm:dispatched for batch 10, which is in the merge queue with no dev in flight. Serial dispatch: this card's dev is the seat's one subagent.
{
"issue": 12082,
"status": "done",
"branch": "claude/issue-12082-affordance-grant-map",
"pr": "https://github.com/objectstack-ai/objectui/pull/12084",
"session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent mode: the parent seat's id)",
"premise_still_valid": true,
"summary": "One affordance-to-grant map now lives in @object-ui/core (AFFORDANCE_GRANTS + resolveAffordance / resolveFieldAffordance / formFieldsAffordance): 18 rows, each naming the CRUD-affordance bit, the object grant and (for field rows) the field question; the verdict is managed-object policy AND effective API operations (getObjectApiOperations, kept inside the map) AND the caller's grant, predicates only when allowed. Core was chosen over permissions because every reader already depends on both and permissions depends on types alone (homing it there adds a permissions-to-core edge; in core the principal is structural, no edge either). The p1 card: checkField gained 'create' (explicit entry, else allowCreate) = the server's insert rule read in objectstack main 76bc1e03 (plugin-security step '2.5. Field-Level Security write enforcement' via computeForbiddenFieldWrites -> FieldMasker.detectForbiddenWrites/getNonEditableFields on insert and update alike; PermissionEvaluator.getFieldPermissions builds entries only for named fields; OPERATION_TO_PERMISSION insert -> allowCreate); every ObjectForm layout's render gate, outbound filter and form-wide lock read the form's row, and a create-mode MasterDetailForm's line cells ask the create question of the child. Measured on main 1b2d0160f: the p1 pin was red on all 9 layouts with body {} (24 failed / 9 passed), now 35/35. Item 1 does not reproduce from objectui's side: for hotclm's clm_payment_plan (controlled_by_parent, no managedBy/userActions/apiMethods) the only principal-dependent header input is the explain record verdict; objectstack-ai/objectstack#22529 (merged 2026-10-09, not an ancestor of the 17.7.0 tag commit 4e4e8814, control leg exit 0) made explain's update verdict on controlled_by_parent records come from the master-detail write check - its own table shows allowed:true + record.visible:false + PATCH 200, the hotclm shape; the header now also reads the update grant (pinned). Item 5 does not reproduce: both list surfaces already ANDed can(create) on main and in 17.7.0, and hotclm binds every position to clm_requester (src/security/bind-position-sets.ts), which grants allowCreate on clm_contract, so the records manager's effective grant allows create; pinned through the map anyway. Item 6 reproduced on main (3 red / 2 green against main's LookupField) and is fixed: Create new reads the target's lookupCreateNew row. Census readers converted: RecordDetailView, ObjectView, ObjectDataPage, importTargetFields (now the create question), RelatedRecordActionsBridge, RecordAttachmentsPanel, ListView, ObjectGrid + rowCrudAffordances (now takes the map's verdicts), ImportWizard, DetailView (operation set + grant; the object's own policy stays the host's channel per objectui#4419), record:details (now reads the update grant), console ProfilePage. Out of family: managedByEmptyState (empty-state copy from the bucket, shows/hides nothing), useFieldPermissions canWrite/writableFields (the resolver's own API), read gates. Known gap, outside the brief's grep population because they read NO grant: plugin-view ObjectView create button, plugin-calendar quick-create and drag-to-reschedule, plugin-kanban card move, LineItemsPanel add/remove lines - see out_of_scope_findings and open_questions.",
"tests": "All at branch head 62413c986 unless noted (package suites ran at 9104511cc; later commits touched only docs, the changeset and 3 pin files, re-run green at fc8e0193f and 62413c986). Package suites (vitest from repo root, under os-verify-lock): core+permissions 209 files / 4075 passed 27 skipped; fields 244 passed 1 skipped / 3973 passed; plugin-list 125 / 1384; plugin-grid 43+154 = 197 files / 373+1478 passed; plugin-detail 156+94 files (1 skipped) / 1397+1036 passed; plugin-form 171 / 2069 passed 1 skipped; app-shell scoped to the touched views (59+60+85 = 204 files, 416+560+964 passed); console scoped ProfilePage 3 files / 29 passed; scripts/__tests__ 179 passed 2 skipped / 5452 passed. Pins at 62413c986: createFormGrant-12082 35/35, affordanceGrantMap-12082 27/27 (+ check-hand-rolled-comment-mask 22/22), LookupField.createGrant-12082 5/5; family pins green: closedAffordanceNotice-11000, RecordAttachmentsPanel.affordanceGrant-12047, useRecordEditable.principalScope (10107), rowRecordCrudVerdict + rowCrudAffordances + rowCrudEffectiveOps (4296) - 9-file final pin run 161/161. Measured on main first: p1 pin vs main 24 failed / 9 passed (every field locked, create body {}); item-6 pin vs main's LookupField 3 failed / 2 passed (controls green), restored blob-equal. Ablations (scripts/ablation-replace.mjs, WRAP mode, predictions first, every restore blob == HEAD and git diff HEAD empty; vitest aliases @object-ui/core to src, so no dist leg): A1 createFormFields field 'create' -> 'write' (put 'write' back): predicted 16+3 red, observed 19 failed / 43 passed; A3 plant can(referenceTo, 'create') in LookupField: census red naming packages/fields/src/widgets/LookupField.tsx, 1 failed / 26 passed as predicted; A4 recordEdit grant 'update' -> 'create': 4 failed / 38 passed as predicted (row table, create-only and edit-only shapes, header create-only row); A5 MasterDetailForm formMode forced 'edit': 1 failed / 34 passed as predicted. Reverse type check against REBUILT .d.ts: a throwaway probe in plugin-form refused resolveAffordance('noSuchAffordance') with TS2345 listing the map's rows, accepted checkField(o, f, 'create') and refused 'bogus' with TS2345 naming 'read' | 'write' | 'create'; probe deleted. Eager closure: base 1b2d0160f 3,244,032 B gz vs head 3,244,687 B gz (+655 B gz, +1,711 B raw, 290/290 eager chunks), check:eager-closure exit 0 at head (3168.6 KB, budget 3204.6 KB).",
"mcp_calls": "0 GitHub MCP calls. 1 session tool call: mcp__claude-code-remote__add_repo (objectstack-ai/hotclm, access read) - answered read_available, attached nothing; used for an anonymous shallow clone into the scratchpad to read hotclm's metadata for items 1 and 5.",
"api_writes": "3 REST writes, each one fleet-write relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create -> POST /repos/objectstack-ai/objectui/pulls (#12084, draft; read-back 7694 B identical); label-write --assign os-support-ai -> POST /repos/objectstack-ai/objectui/issues/12084/assignees (read-back matches); this os-dev-report -> POST /repos/objectstack-ai/objectui/issues/12082/comments. Plus git push to claude/issue-12082-affordance-grant-map (not REST).",
"open_questions": [
{
"question": "Five write affordances read NO grant at all, so the brief's grep census (grant reads) cannot see them: plugin-view ObjectView renderToolbar showCreateButton (create), plugin-calendar ObjectCalendar handleDateClickDefault / handleTimeRangeSelectDefault quick-create (create) and handleEventDropDefault drag-to-reschedule (update), plugin-kanban ObjectKanban handleCardMove (update), plugin-form LineItemsPanel add / remove lines (create / delete on the child; only schema.readonly). They are this family's members; how are they closed?",
"options": [
"A - a patch round on this claim: give each a map row (or reuse listNew / rowEdit / rowDelete) and read it; claim's file surface amended to plugin-view, plugin-calendar, plugin-kanban, LineItemsPanel",
"B - a follow-up dispatch on this card after #12084 lands, keeping this PR's size as is",
"C - leave them: the server refuses the write anyway"
],
"recommendation": "B, because each surface needs its own runtime pin (calendar and kanban are heavy suites) and #12084 already spans 9 packages; C contradicts the Zone 1 ruling that every affordance reads the map. Note the census pin cannot catch this shape by construction - it refuses grant reads outside the map, not affordances with no grant read - which the follow-up should state rather than imply."
}
],
"out_of_scope_findings": [
"class: a (same family, folds into this close-out card - no separate card) - reach: NOT MEASURED at a public door, source read only on objectui main 1b2d0160f - evidence: plugin-view ObjectView renderToolbar `showCreateButton = schema.showCreate !== false && operations.create !== false`; plugin-calendar ObjectCalendar handleDateClickDefault / handleTimeRangeSelectDefault open quick-create on calendarConfig + objectName + dataSource.create only, handleEventDropDefault persists via dataSource.update; plugin-kanban ObjectKanban handleCardMove writes the group field with no grant read; LineItemsPanel add / remove lines gate on schema.readonly only - dedupe words: quick-create grant, drag reschedule permission, kanban card move permission, object-view create button permission, line items add remove grant",
"carrier: hotclm seat (repo:hotclm), noted not filed - objectui#12081 item 1's cause is the explain record verdict for controlled_by_parent rows, fixed upstream by objectstack-ai/objectstack#22529 after the 17.7.0 tag; hotclm's finance header Edit should be re-measured once its @objectstack pin includes that merge",
"carrier: hotclm seat, noted not filed - objectui#12081 item 5 reads one permission set; the records manager's effective grant on clm_contract includes clm_requester's allowCreate (every position binds clm_requester), so New / Import are correct for that user",
"carrier: 承接者:无 - noted not filed - MePermissionsProvider.check keys objects by the name as given while checkField and getObjectApiOperations lowercase it; every reader passes the object's own lowercase name, no divergence measured",
"carrier: 承接者:无 - noted not filed - LookupField reads `allow_create ?? allowCreate` (two spellings of one key), a consumer-side alias outside this card",
"carrier: 承接者:无 - noted not filed - ObjectGrid's comment above rowActionDefsList carries a pre-existing cross-file line address (index.tsx plus a line number); not swept here per AGENTS.md #11"
],
"gates": [
{
"command": "turbo run build --filter=@object-ui/app-shell... (and console^..., and cli/plugin-ai/gantt/map/markdown/timeline closures) --concurrency=2",
"exit": 0,
"verdict": "Tasks: 29/29, 34/34, 20/20 successful"
},
{
"command": "pnpm --filter @object-ui/{core,permissions,plugin-form,fields,plugin-list,plugin-grid,plugin-detail,app-shell} type-check",
"exit": 0,
"verdict": "each echoed its own type-check script, 0 error TS (tsc --noEmit && tsc -p tsconfig.test.json)"
},
{
"command": "pnpm --filter @object-ui/console type-check",
"exit": 0,
"verdict": "> @object-ui/console@17.7.0 type-check, no error"
},
{
"command": "pnpm exec vitest run (package suites listed under tests)",
"exit": 0,
"verdict": "all Test Files passed (counts in tests)"
},
{
"command": "pnpm exec eslint (39 touched .ts/.tsx)",
"exit": 0,
"verdict": "0 errors; warnings on added lines: no-explicit-any in new test files and a pre-existing react-hooks/preserve-manual-memoization that moved with LookupField's deps line (9 before, 9 after)"
},
{
"command": "pnpm check:control-bytes",
"exit": 0,
"verdict": "check-control-bytes: OK (scanned 8277 tracked text file(s))"
},
{
"command": "pnpm check:test-path-roots",
"exit": 0,
"verdict": "check-test-path-roots: OK"
},
{
"command": "pnpm check:changeset-claims",
"exit": 0,
"verdict": "No pending changeset names a file this change touches."
},
{
"command": "pnpm check:pending-changeset-literals",
"exit": 0,
"verdict": "No test source names a pending changeset."
},
{
"command": "node scripts/check-changeset-presence.mjs",
"exit": 0,
"verdict": "39 source file(s) of 9 released package(s) changed, and this change declares 1 changeset(s)"
},
{
"command": "pnpm check:new-line-citations",
"exit": 0,
"verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0"
},
{
"command": "pnpm check:readme-exports",
"exit": 0,
"verdict": "check-readme-exports: OK (585 of 870 import bindings judged, 0 fabricated); first run exit 1 = 6 unbuilt packages, rebuilt and re-run"
},
{
"command": "pnpm check:doc-fences / doc-types / doc-snippets / doc-examples",
"exit": 0,
"verdict": "each passed"
},
{
"command": "pnpm check:vi-mock-specifiers / vi-mock-inherit / vi-mock-override-shape",
"exit": 0,
"verdict": "each passed"
},
{
"command": "pnpm check:phantom-deps / unused-deps / self-import / esm-specifiers / unreferenced-sources",
"exit": 0,
"verdict": "Every in-scope import is declared by the package that publishes it (phantom-deps); the rest passed"
},
{
"command": "pnpm check:comment-mask-corpus",
"exit": 0,
"verdict": "1 file(s) disagree, within the residue objectui#7882 is holding open"
},
{
"command": "pnpm check:eager-closure (after apps/console vite build)",
"exit": 0,
"verdict": "Console eager closure is 3168.6 KB gzipped across 290 of 2474 chunks (budget: 3204.6 KB, headroom: 35.9 KB)"
},
{
"command": "pnpm exec vitest run scripts/__tests__/",
"exit": 0,
"verdict": "179 passed, 2 skipped"
},
{
"command": "CI on #12084",
"exit": null,
"verdict": "in_progress at 62413c986; at fc8e0193f Test (shard 4/8) failed on check-hand-rolled-comment-mask (census used a private comment regex) - fixed in 62413c986, the other shards were cancelled by that push"
},
{
"command": "i18n gates (check:i18n-keys / drift / dead-keys / designer-parity)",
"exit": null,
"verdict": "not run: no locale pack changed (no new keys; the notice copy already existed)"
}
],
"deviations": [
"The PR body was written once, when the draft opened after the first commit (os-dev rule: no PATCH by dev); it predates the gate readout, the ablations, the eager delta, the final reader list and the fix for check-hand-rolled-comment-mask. Requested amendment for the seat: replace the Readers section with the converted list in summary plus the out-of-family list (managedByEmptyState, useFieldPermissions, read gates), add the MasterDetailForm line-cell change, the tests/ablation/eager readings in tests, and the no-grant family members from open_questions under Acceptance notes.",
"Files beyond the claim's named surface, each a census reader the claim says to name in the PR: plugin-detail DetailView + record-details, plugin-grid ImportWizard + rowCrudAffordances, apps/console ProfilePage (+ its 3 test stubs), the core/permissions READMEs and content/docs/plugins/plugin-form.mdx (AGENTS.md #2). MasterDetailForm (create-mode line cells ask the create question) was taken under the bounded in-place rule (same defect class, mechanical, pinned by createFormGrant-12082 + ablation A5, no other claim on the file); the claim's file surface needs that addition.",
"Behaviour moves beyond the card's rows, all toward the server's refusal: the grid add-record row now also honours managed-object policy and the effective create operation (gridAddRow row); DetailView's object gate adds the effective operation set; record:details in-place editing now reads the update grant; the form-wide lock and its notice now also engage on a denied grant for the form's mode.",
"The census pin is a test-level guard the card and the claim rule for (an affordance or reader outside the map turns it red); no new check:* gate or workflow was added.",
"My hand-derived gate list missed scripts/__tests__/check-hand-rolled-comment-mask.test.ts (a root-level test that scans package tests); CI shard 4 caught it and it is fixed. The root scripts suite was then run locally in full.",
"Read access to objectstack-ai/hotclm via add_repo (read_available, nothing attached) and an anonymous shallow clone in the scratchpad, to measure items 1 and 5 against hotclm's metadata. A temporary ref refs/os-dev-12082/main was written to the shared .git to read origin/main's delta and deleted after.",
"Commit trailers are the model-free pair per objectui AGENTS.md (Claude-Session + Co-authored-by: Claude), not the harness's model-named form."
],
"files_changed": [
".changeset/12082-affordance-grant-map.md",
"apps/console/src/pages/system/ProfilePage.tsx",
"apps/console/src/pages/system/__tests__/ProfilePage.access.test.tsx",
"apps/console/src/pages/system/__tests__/ProfilePage.language.test.tsx",
"apps/console/src/pages/system/__tests__/ProfilePage.sharedSelect-11865.test.tsx",
"content/docs/plugins/plugin-form.mdx",
"packages/app-shell/src/views/ObjectDataPage.tsx",
"packages/app-shell/src/views/ObjectView.tsx",
"packages/app-shell/src/views/RecordAttachmentsPanel.tsx",
"packages/app-shell/src/views/RecordDetailView.headerActionGates.test.tsx",
"packages/app-shell/src/views/RecordDetailView.tsx",
"packages/app-shell/src/views/RelatedRecordActionsBridge.tsx",
"packages/app-shell/src/views/importTargetFields.test.ts",
"packages/app-shell/src/views/importTargetFields.ts",
"packages/core/README.md",
"packages/core/src/index.ts",
"packages/core/src/utils/affordanceGrants.ts",
"packages/fields/src/widgets/LookupField.createGrant-12082.test.tsx",
"packages/fields/src/widgets/LookupField.tsx",
"packages/permissions/README.md",
"packages/permissions/src/MePermissionsProvider.tsx",
"packages/permissions/src/PermissionContext.ts",
"packages/permissions/src/PermissionProvider.tsx",
"packages/plugin-detail/src/DetailView.tsx",
"packages/plugin-detail/src/renderers/record-details.tsx",
"packages/plugin-form/src/DrawerForm.tsx",
"packages/plugin-form/src/MasterDetailForm.tsx",
"packages/plugin-form/src/ModalForm.tsx",
"packages/plugin-form/src/ObjectForm.effectiveOps.test.tsx",
"packages/plugin-form/src/ObjectForm.tsx",
"packages/plugin-form/src/SplitForm.tsx",
"packages/plugin-form/src/TabbedForm.tsx",
"packages/plugin-form/src/WizardForm.tsx",
"packages/plugin-form/src/affordanceGrantMap-12082.test.tsx",
"packages/plugin-form/src/createFormGrant-12082.test.tsx",
"packages/plugin-form/src/fieldWriteGate.ts",
"packages/plugin-form/src/formChrome.i18n-11071.test.tsx",
"packages/plugin-form/src/sanitize.ts",
"packages/plugin-grid/src/ImportWizard.tsx",
"packages/plugin-grid/src/ObjectGrid.tsx",
"packages/plugin-grid/src/__tests__/rowCrudAffordances.test.ts",
"packages/plugin-grid/src/rowCrudAffordances.ts",
"packages/plugin-list/src/ListView.tsx"
]
}
This amends claim 6093729821 after the dev's report 6094735932 (status: done, PR objectui#12084 at 62413c98).
1. The dev's open question: five write affordances read no grant at all
These five sit outside the brief's grep census, which looked for grant reads:
plugin-view ObjectView create button;
plugin-calendar quick-create;
plugin-calendar drag-to-reschedule;
plugin-kanban card move;
plugin-form LineItemsPanel add / remove lines.
Seat answer: B. They are closed by a follow-up dispatch on this card, after objectui#12084 lands. This is a sequencing call, so it is in the class the seat answers without escalating.
Why not A: each surface needs its own runtime pin (calendar and kanban are heavy suites), and objectui#12084 already spans nine packages.
Why not C: "the server refuses the write anyway" contradicts this card's ruling that every affordance reads the map.
Consequences:
objectui#12084 stops closing this card. The seat rewrites its first line from Fixes #12082 to Part of #12082, so the card stays open for the remainder.
After the landing: the claim is released to pm:queue with pm:retriage, and the remaining scope written on the card. The p1 defect is closed by objectui#12084, so triage re-grades what is left.
For the follow-up: each of the five gets a row in the map (or reuses an existing one, for example listNew, rowEdit or rowDelete), and its reach is measured first. The census pin as it stands refuses grant reads outside the map; it cannot see an affordance that reads no grant. The follow-up must state that limit or close it. It must not imply the pin covers that shape.
2. File surface: added, as census readers the claim said to name
packages/plugin-detail/src/DetailView.tsx and src/renderers/record-details.tsx;
packages/plugin-grid/src/ImportWizard.tsx and src/rowCrudAffordances.ts;
apps/console/src/pages/system/ProfilePage.tsx and its three test stubs;
packages/plugin-form/src/MasterDetailForm.tsx: a create-mode form's line cells ask the create question of the child. Same defect class, pinned by createFormGrant-12082 and ablation A5;
Draft against main, based on 1b2d0160. git merge-tree against main12ff2563 (objectui#6349 batch 10 landed since) is clean.
The body was brought to round 2 by the seat. First line Part of #12082, per the seat's answer B (6094765692): no closing keyword in the body or in any of the ten commits, so this card stays open for its remainder. Clause-②: yes at line start.
The commits carry only the model-free trailer pair. Assignee os-support-ai, set with no refusal.
Scope: 43 files, +1714/−284, on the claim 6093729821 as amended in 6094765692 (census readers in plugin-detail, plugin-grid, the console ProfilePage, and MasterDetailForm's create-mode line cells). The path fork reads no governed path, and the change is under the 5,000-line human-merge threshold.
What lands:
The p1 defect: a create form asks its fields the create question. checkField(object, field, 'create') answers from the explicit field entry when there is one, and from allowCreate otherwise. That follows the server's insert rule, read at objectstack 76bc1e03: field step 2.5 refuses only an explicit editable: false, and object admission maps insert to allowCreate. Measured on main first: 24 red / 9 green across all nine layouts, with the create body {}. At the head: 35 / 35.
The map:AFFORDANCE_GRANTS in @object-ui/core, with 18 rows. resolveAffordance returns managed-object policy ∧ effective API operation set ∧ the caller's grant, and surfaces predicates only when allowed. resolveFieldAffordance and formFieldsAffordance cover the form rows. Every census reader now reads it, and the PR names the out-of-family readers with their reasons.
objectui#12081 item 6 (lookup "Create new"): reproduced on main (3 red) and fixed through the lookupCreateNew row, asked of the target object.
Items 1 and 5: do not reproduce from objectui's side (readings in the PR). Both rows are pinned through the map anyway.
Behaviour moves beyond the card's rows all point toward the server's refusal, and the PR lists each one.
Changesets: @object-ui/core and @object-ui/permissionsminor, seven packages patch. Fail-open with no provider is unchanged.
Contract review:6094907310 on the PR, rendered at CONTRACT_REVIEW_TIER by an isolated, read-only reviewer on this head, reads PASS. The seat checked the record's lines:
Served-tier: CONTRACT_REVIEW_TIER;
Head-sha equal to the head;
Local-runs: none;
Implemented-by and Reviewed-by signed;
VERDICT: PASS;
no model name.
It judged:
① the 18 rows name the right grants, and the create question matches the server's insert rule; the re-signatured helpers and PermissionContextValue are not exported from their package entries, so the widened checkField union is purely additive; every console object form sets mode, so mode === 'create' reaches every create surface;
② minor / patch matching what the diff publishes;
③ the open question answered by the seat (B), and every deviation closed on this head.
Gates:
The dev's local suites all pass (counts in 6094735932). The four ablations went red as predicted, with restores blob-equal.
The Console's eager closure adds 655 B gzipped; check:eager-closure passes.
CI on 62413c98: 43 runs, 40 success, 3 skipped (the expected three), 0 failure; mergeable_state clean.
Process note, acknowledged here: the dev wrote a temporary ref, refs/os-dev-12082/main, into the shared checkout's .git to read origin/main's delta, then deleted it. That is a write to a shared checkout, which the brief forbids. It has no effect on the diff or the head. The reviewer raised it, and the seat records it rather than letting it pass silently.
Out-of-scope findings, one line each:
five write affordances that read no grant: this card's follow-up (answer B), not a new card;
objectui#12081 items 1 and 5, carried to the hotclm seat: noted in the PR, to be re-measured there;
Landing: ready + auto-merge through the queue, now. Its merge does not close objectui#12082. At landing, the seat releases the claim to pm:queue with pm:retriage, writes the remaining five affordances on the card, and records items 1, 5 and 6 on objectui#12081.
Release: session_01CGZy1BGCjdN5cXqL9cnvB8 (account os-support-ai) releases claim 6093729821, as amended in 6094765692.
Reason: the dispatched round has landed. The card stays open on purpose: the PR said Part of #12082, per the seat's answer B. The maintainer has also told this seat to clock out once this merge lands, verbatim, 2026-10-10: 「当前任务处理完,合并后就下班。」 So no follow-up is dispatched from this seat.
Next:pm:dispatched and the assignee are removed in this act. The card goes back to pm:queue, and pm:retriage is added for the question below.
What remains on this card (the follow-up): five write affordances read no grant at all, so the first round's census (grant reads) could not see them:
plugin-form LineItemsPanel add / remove lines: create / delete on the child.
For each: measure reach on main first, then give it a row in the map (or reuse listNew, rowEdit or rowDelete) and read it through resolveAffordance. The census pin as it stands refuses grant reads outside the map; it cannot see an affordance that reads no grant. The follow-up states that limit or closes it.
pm:retriage — what this seat asks of triage: re-grade the remainder. priority:p1 was for the create form, which a create-only role could not use at all, and that is fixed on main. The five above show an affordance whose write the server then refuses, which is the family's p2 shape (objectui#12081's triage). Say whether the remainder stays on this card or moves to a successor card.
The wait is spent. The block (6110279954) waited on epic objectui#7611's C9 part 2, "which owns those files" (seat 2's landing note 6109930881). All of C9 has landed since then:
C9 part 2, the suggested-bindings panel: objectui#12148 (6111491126, 2026-10-11T17:04Z).
What is left is as recorded. On objectui main8a8bcf3dde, packages/plugin-form/src/affordanceGrantMap-12082.test.tsx:683–:690 still holds exactly the four unmapped Setup assignment entries (permission-set and position, create and delete).
Done when: WRITE_SITES has no unmapped entry.
For that round: reviewer note (e) of 6109740908 still applies.
Claim: PM loop round 2 (the four Setup assignment entries, the card's last unmapped census entries; triage 6213818308 and 6102063887; reviewer note (e) of 6109740908)
Session: session_01Vk15bz8xHtKGUfXfCm16h4
Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-12082-setup-assignment-census
Worktree: objectui-issue-12082
Domain: domain:ui
Seat: domain:ui#3
File surface: the census pin packages/plugin-form/src/affordanceGrantMap-12082.test.tsx (the four Setup unmapped entries become mapped or outOfFamily, plus the census's own bookkeeping); packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx and PositionHoldersSection.tsx (only where a site that a grant-less user can reach gets its affordance gated); packages/core/src/utils/affordanceGrants.ts (a row only where none fits); the record feed pin beside packages/app-shell/src/views/RecordDetailView.tsx (reviewer note (e): one case with the registered sys_comment and sys_comment_reaction schemas in objects); tests beside each; the docs that name the map's rows; and one .changeset/12082-*.md (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (default tier; permission-affordance semantics with a reach measurement per site; dispatch-gates --tier --repo objectstack-ai/objectui over the named paths: no path-derived mandate)
Clause-②: no (a prediction, as in claim 6108058067: if a row is added to AFFORDANCE_GRANTS, the published value widens, the report says so, this line is corrected to yes, and the review runs at CONTRACT_REVIEW_TIER as 6109740908 did)
Responsibility: @object-ui/app-shell's Setup permission-set and position assignment write sys_user_permission_set / sys_user_position rows and read no grant on those objects | AFFORDANCE_GRANTS and resolveAffordance in @object-ui/core, and the WRITE_SITES census; Setup's own route gate may already be the boundary (triage's likely case) | a user who reaches Setup's assignment sections without the object grant; reach is measured per site first
Thread-read: 6213818308
Serial constraints cleared: no in-flight lane claim and no open objectui PR names AssignedUsersSection.tsx, PositionHoldersSection.tsx, affordanceGrants.ts, affordanceGrantMap-12082.test.tsx or RecordDetailView.tsx (read 2026-10-16T12:08Z). objectui#12406 (seat 1) holds metadata-admin/PermissionAdvancedFacets.tsx and metadata-admin/i18n.ts; this card touches that i18n.ts only for a new string, and if it must, whichever lands later merges main. objectui#12606 (seat 4) holds metadata-admin/ResourceListPage.tsx only.
{
"issue": 12082,
"status": "done",
"branch": "claude/issue-12082-setup-assignment-census",
"pr": "https://github.com/objectstack-ai/objectui/pull/12685",
"session": "session_01Vk15bz8xHtKGUfXfCm16h4 (subagent mode: the dispatching seat's id; the relay resolved the same id)",
"premise_still_valid": true,
"summary": "The census's last four unmapped WRITE_SITES entries (Setup's permission-set holders addUsers/removeUser on sys_user_permission_set, position holders addHolders/removeHolder on sys_user_position) were graded on measured reach. All four are reachable without the grant, which falsifies triage's likely case: Setup's own gate is not the boundary. The Setup app asks setup.access, its Positions and Permission Sets nav entries ask nothing more, and objectstack's shipped organization_admin set (plus its wall-less derivation) holds setup.access while it is read-only on both assignment objects; the server's CRUD step refuses that caller's write. So each section now reads two EXISTING rows asked of its assignment object: Add reads relatedNew, Remove reads relatedRowDelete (the rows a related list's Add existing and row Delete read). A closed row withholds the Add button, its picker and each row's Remove; the holders still list; no provider reads open. The census grades all four mapped and UNMAPPED_BACKLOG is empty (the card's done-when), so the PR body's first line carries the closing keyword for this card. No AFFORDANCE_GRANTS row is added, so the claim's Clause-2 'no' holds as measured. Reviewer note (e): the feed pin gains one case with the registered sys_comment and sys_comment_reaction schemas in the page objects.",
"tests": "Final head 7e9eed3 (merge of origin/main dcd1041 into 4754b38). Locked run: pnpm exec vitest run --maxWorkers=2 over 43 files (census pin; new setupHoldersGrant-12082.test.tsx; AssignedUsersSection, AssignedUsersSection.contractEnvelope-5945 and PositionHoldersSection suites; every PermissionMatrixEditor.* suite; ResourceEditPage.setupCatalog-7611, ResourceEditPage.tabDeepLink-12496, trailDoorsGate-12475, walledCatalogReadOnly-12183, capabilityGateChannel-7234, ResourceListPage.environmentScope-7611, PermissionAdvancedFacets.adminScopeAnchor; RecordDetailView.feedGrant-12082 and feedCommentEditDelete-12268; useApproverDirectory.contractEnvelope-5945; the seven app-shell ratchet suites) → 'Test Files 43 passed (43)', 'Tests 373 passed (373)', TESTS4_EXIT=0. The same 42-file set (without the merged PermissionAdvancedFacets suite) at 4754b38: 42 passed, 361 tests. Ablations at 4754b38, one-time, through objectstack scripts/ablation-replace.mjs wrap mode (anchor x1 → x0, blob changed; restore proved: blob == HEAD and git diff HEAD empty; tree porcelain 0 after): A1 AssignedUsersSection canAdd → true: red, 'Tests 5 failed | 58 passed (63)' (4 section cases: org admin, read-only, delete-only, operation set; plus the census 'every mapped site names rows of the map, of its own verb, each read by one of the files it names'). A2 PositionHoldersSection canRemove → true: red, 'Tests 5 failed | 58 passed (63)' (org admin, read-only, create-only, operation set; same census assertion). A3 note (e): the registered sys_comment schema given userActions create false: red, 'Tests 1 failed | 18 passed (19)', only the new case, on expect(composer(view)).not.toBeNull(). The first A3 attempt was a no-op, void: the replacement carried its own anchor, so the tool refused (anchor 1 → 1) and restored; re-run with an anchor-altering replacement. Direction observed in all three: red (the common case). Subjects resolve through relative imports and vitest src aliases, so no dist preflight applies.",
"gates": "Local subset, objectui (hand-derived; objectui has no dispatch-gates.mjs). At 7e9eed3: turbo run build --filter='@object-ui/app-shell...' --concurrency=2 BUILD4_EXIT=0 ('Tasks: 29 successful, 29 total'); pnpm --filter @object-ui/app-shell type-check TC_APP4_EXIT=0 and pnpm --filter @object-ui/plugin-form type-check TC_FORM4_EXIT=0 (both echo 'tsc --noEmit && tsc -p tsconfig.test.json'; tsconfig.test.json includes src/**/*.test.tsx); node scripts/check-changeset-presence.mjs exit 0 ('5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'); pnpm check:new-line-citations exit 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)'); raw control-byte grep of the 8 changed files: none (grep exit 1). eslint on the 5 changed source/test files: exit 0, 0 errors, 30 warnings, one on an added line (the pre-existing onSelectRecords any, re-indented under canAdd); run at 4754b38, files byte-identical at 7e9eed3. pnpm check:readme-exports: exit 1, PARTIAL: 72 self-imports could not be judged, all in packages outside the built closure (cli, plugin-ai, plugin-gantt, plugin-map, plugin-markdown, plugin-timeline); zero findings name packages/core/README.md or packages/app-shell/README.md; whole-tree verdict is CI's README Export Check. CI at 7e9eed3, read once at PR open (not waited on): 35 check-runs, 7 success (Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, label), 3 skipped by design (Test (coverage shard ...), Test (coverage), dependabot), 25 in_progress (Lint, Type Check, Repo Gates, Build & E2E, Build Docs, Bundle Analysis, Changeset Claim Re-read, Test (dist pins), Test (shard 1..16/16), Live E2E (informational)). NOT MEASURED: the remaining 467 metadata-admin suites and the rest of the farm (CI's).",
"mcp_calls": "0",
"api_writes": "2 relay strokes = 2 x POST /repos/objectstack-ai/objectstack/dispatches (fleet-write), executing 3 REST writes on objectstack-ai/objectui as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectui/pulls (draft, PR 12685), POST /repos/objectstack-ai/objectui/issues/12685/assignees (os-elon-musk), POST /repos/objectstack-ai/objectui/issues/12082/comments (this report). Plus git push x3 of the branch (probe, 4754b38, 7e9eed3), not REST. No label write, no card assignee or label write.",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: exception: release-text (published in @objectstack/spec 18.0.0-next.2 dist/migrations, which objectui's install carries) · evidence: objectstack's semantic migration entry walled-catalog-tenant-writes-retired says in its replacement text that 'an organization's administrators assign it to their own members through sys_user_position and sys_user_permission_set, which are unchanged', while the shipped organization_admin set and its derived organization_admin_no_bypass are read-only on both objects (objectstack pin 'organization_admin is read-only on global RBAC tables to prevent privilege escalation'), and the security middleware's step 2 CRUD check refuses that write. One of the two declarations is wrong, and which one is an objectstack product call (anti-escalation rule versus the ADR-0131 D3 assignment promise). Seam: spec:migrations entry walled-catalog-tenant-writes-retired (replacement) → runtime: plugin-security default-permission-sets organization_admin objects entries / security-plugin CRUD step · dedupe words: walled-catalog-tenant-writes-retired, organization_admin read-only sys_user_permission_set, organization administrator assign holders, upgrade guide replacement text",
"carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed: RecordDetailView still mounts RecordPermissionAssignmentsRenderer on the sys_permission_set record page, an object objectstack main no longer registers (migration entry catalog-row-objects-retired), so that mount cannot load against a current server: dead code",
"carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed: each section reads the object grant (a necessary condition), not the delegated-administration gate's adminScope, which can still refuse a holder of the grant; the same object-verdict posture as every row of the map"
],
"files_changed": [
".changeset/12082-setup-holders-grant.md",
"packages/app-shell/README.md",
"packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx",
"packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx",
"packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx",
"packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx",
"packages/core/README.md",
"packages/plugin-form/src/affordanceGrantMap-12082.test.tsx"
],
"line_budget": "n/a: no skills/** or governed ledger path touched; diff vs base f78efdd over the 8 files: 476 insertions, 93 deletions",
"deviations": [
"File surface: every changed path is in the claim's surface. The app-shell README's Setup catalog section and PositionHoldersSection's docblock are counted under 'the docs that name the map's rows' and the component; it also dropped that docblock's claim that a tenant administrator assigns under a wall. affordanceGrants.ts is untouched (no row needed). The feed pin's mount() helper gained an objects parameter for the note (e) case. metadata-admin/i18n.ts untouched (no new string).",
"The first locked run selected the whole metadata-admin directory (510 test files, far wider than the seam). After 16 minutes holding the shared lock with no output, I stopped my own vitest process (PID 26086, ancestry verified to my lock run 24744); TESTS_EXIT=143 is that kill and reads NOT MEASURED. Replaced by the declared narrowed 42/43-file selection; CI runs the rest.",
"Two lock acquisitions hit queue-timeout (exit 99, slot kept) and were re-taken under the same slot; not a failure.",
"git fetch origin main:refs/os-12082/main in the shared /home/user/objectstack checkout also advanced that checkout's shared refs/remotes/origin/main (34b740f2d..570f01d27) as an opportunistic update; no effect on any diff. My named refs (refs/os-12082/main there, refs/os-12082/branch in objectui) are deleted.",
"Live HTTP measurement of the org admin's refusal: NOT MEASURED. Reason: no built objectstack stack in this container, and building and booting it is outside this card. Reach is read from objectstack main 570f01d27 source plus its own pin.",
"Commit trailers are the model-free pair (AGENTS.md rule), not the harness reminder's model-bearing Co-Authored-By; the merge commit was amended before push to carry them. The PR footer uses os-dev's session-URL form.",
"check:readme-exports is a partial reading (see gates); the prerequisite build covers app-shell's closure only.",
"Worktree /home/user/objectui-issue-12082 removed after the PR opened (node_modules first, no --force). The local branch ref stays in the shared objectui .git, matching the pushed head 7e9eed3."
]
}
{
"issue": 12082,
"status": "done",
"branch": "claude/issue-12082-setup-assignment-census",
"pr": "https://github.com/objectstack-ai/objectui/pull/12685",
"session": "session_01Vk15bz8xHtKGUfXfCm16h4 (subagent mode: the dispatching seat's id)",
"premise_still_valid": true,
"summary": "Patch round on PR 12685: the one gap the seat's browser check measured. AssignedUsersSection's local copy table splits `empty` into the statement (en 'No users assigned yet.', zh '还没有分配任何用户。') and the click-Add instruction `emptyAddHint` (en 'Click \"Add user\" to assign.', zh '点击「添加用户」来分配。'), joined by a copy-local `sentenceGap` (a space in en, none in zh). The instruction renders only under `canAdd`; the statement renders either way. With `canAdd` open the rendered sentence is byte-identical to the old copy in both locales (the ablation's Received lines show it). `metadata-admin/i18n.ts`, `PositionHoldersSection` and the three out-of-round items are untouched. Base: `git merge-tree --write-tree HEAD origin/main` against origin/main 15d8ca6 exited 0 with no conflict, so main was NOT merged; the new head is cf08306, one commit on 7e9eed3.",
"tests": "All at head cf08306 (the fix committed before every run). Locked: `pnpm exec vitest run --maxWorkers=2` over 5 files in repo-root form (AssignedUsersSection.contractEnvelope-5945, AssignedUsersSection, PositionHoldersSection, setupHoldersGrant-12082, and plugin-form affordanceGrantMap-12082) → 'Test Files 5 passed (5)', 'Tests 83 passed (83)', TESTS_EXIT=0, 'VERDICT command-exit 0'. Locked verbose run of setupHoldersGrant-12082 → 'Tests 18 passed (18)' (14 earlier cases + 4 new): describe 'AssignedUsersSection's empty state names Add only where Add is offered, in $locale' for 'en-US' and 'zh-CN', each with 'with the create grant closed, it says nobody is assigned and does not say to click Add' (delete-only grant: no Add button, and the empty state's text does not contain the Add label) and 'with the create grant open, it says both, and the button it names is offered' (create-only grant: the Add button is offered and the empty state's text contains its label). The two grants flip create and delete in opposite directions, so a gate on the wrong row reads red too. Ablation A4, one-time, locked, objectstack scripts/ablation-replace.mjs wrap mode: anchor '{canAdd ? c.empty + c.sentenceGap + c.emptyAddHint : c.empty}' x1 → x0, replacement '{c.empty + c.sentenceGap + c.emptyAddHint}' x0 → x1, blob 91688f271f3d → ee5efe8d414f ('ok mutation landed'). Red: 'Tests 2 failed | 16 passed (18)', exactly the two closed-grant cases: en \"expected 'No users assigned yet. Click \"Add use...' not to contain 'Add user'\" and zh \"expected '还没有分配任何用户。点击「添加用户」来分配。' not to contain '添加用户'\"; both open cases stayed green. Restore proved by the tool ('ok restored: blob == HEAD (91688f271f3d) and git diff HEAD is empty') and again by hand: git diff HEAD 0 bytes, status porcelain 0 lines, anchor count back to 1, hash-object == HEAD blob 91688f271f3d. Direction observed: red (the common case). No dist preflight: the subjects resolve through relative imports and vitest src aliases.",
"gates": "Local subset, objectui, the dispatch's list (objectui has no dispatch-gates.mjs, so no --ran reconciliation exists). Closure build, locked: `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build` → 'VERDICT command-exit 0', 'Scope: 29 of 47 workspace projects'. Type-check, locked: `pnpm --filter @object-ui/app-shell type-check` → 'VERDICT command-exit 0', TC_EXIT=0, echoes 'tsc --noEmit && tsc -p tsconfig.test.json' (that program includes 'src/**/*.test.tsx'), 0 'error TS' lines. eslint on the 2 changed files: ESLINT_EXIT=0, 0 errors, 11 warnings, all on pre-existing lines outside the added hunks; the test file has none. `node scripts/check-changeset-presence.mjs` exit 0 ('5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'). `pnpm check:new-line-citations` exit 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0'). Raw control-byte grep of the 2 files: none (grep exit 1). CI at cf08306, read once at 2026-10-16T14:30Z and not waited on: 35 check-runs; 8 success (Changeset Overwrite Report, Changeset Bump Policy, Changeset Fixed Group Check, Line Citation Gate, label, Changeset Declaration, Governed Surface Queue Guard, Changeset Claim Re-read); 3 skipped (Test (coverage), Test (coverage shard matrix), dependabot); 24 in_progress (Lint, Type Check, Repo Gates, Build & E2E, Build Docs, Bundle Analysis, Test (dist pins), Test (shard 1..16/16), Live E2E (informational)). NOT MEASURED: the rest of the farm, reason: CI's.",
"mcp_calls": "0",
"api_writes": "1 relay stroke = 1 x POST /repos/objectstack-ai/objectstack/dispatches (fleet-write), executing 1 REST write on objectstack-ai/objectui as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectui/issues/12082/comments (this report). Plus 1 git push of the branch (7e9eed3..cf08306, fast-forward), not REST. No label, assignee or body write on the card or the PR.",
"open_questions": [
{
"question": "PR 12685's body does not mention the empty-state copy, and its readings stop at 7e9eed3. The seat writes the body; which edit?",
"options": [
"A: under 'What changes', after the bullet that begins 'A closed row withholds the Add button', add: 'The permission set's empty state follows Add. It says No users assigned yet. under every grant; its sentence Click \"Add user\" to assign. (zh 点击「添加用户」来分配。) is said only where Add is offered. The component's own copy table splits the empty state into the two sentences, and metadata-admin/i18n.ts is unchanged. PositionHoldersSection's empty copy names no button and stays.' Then add a section 'Patch round at head cf08306': main not merged (merge-tree against 15d8ca6 clean); 5 suites, 83 tests passed; setupHoldersGrant-12082 18 passed, including the four empty-state cases (en and zh, create closed and open); app-shell type-check exit 0; eslint 0 errors; changeset-presence and line-citation gates exit 0; ablation A4 (instruction rendered unconditionally): 'Tests 2 failed | 16 passed (18)', exactly the two closed-grant cases, restore proved.",
"B: leave the body as is and let the seat's review comment carry this round."
],
"recommendation": "A, because reviewers read the 'What changes' list, and the list should name every behaviour the PR changes; this round's readings belong beside the earlier rounds' readings."
}
],
"out_of_scope_findings": [],
"files_changed": [
"packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx",
"packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx"
],
"deviations": [
"The changeset .changeset/12082-setup-holders-grant.md is left as it is. Nothing in it is now untrue: it says a closed row withholds the Add button, the picker and each Remove, and that the holders still list. It does not name the empty-state sentence, and the dispatch allows prose changes only to correct untrue text.",
"The closure build ran through the lock as a nohup launch, waited on with a foreground `tail --pid`, so that a long build stays outside the foreground cap; its VERDICT line is read from the log. Every other locked run was in the foreground.",
"The merge-tree output went once to a /tmp file before I switched to the per-issue scratchpad directory. That file is deleted.",
"Commit trailers are the model-free pair from AGENTS.md, not the harness reminder's model-bearing Co-Authored-By.",
"Worktree /home/user/objectui-issue-12082 is removed (node_modules first, then git worktree remove without --force, after git status was empty). The local branch ref in the shared objectui .git stays at cf08306, the pushed head."
]
}
ACCEPT → PR #12685 comment 6218195489 · domain:ui seat 3 · session_01Vk15bz8xHtKGUfXfCm16h4 · 2026-10-16T14:55Z. The seat landed it through the relay (pr_ready + automerge_enable); on MERGED, Fixes closes this card. Filed from its browser check: objectstack-ai/objectstack#23960 and objectui#12706.
Activity
objectstack-fleet commentedon Oct 10, 2026
Triage: first grade,
bug·priority:p1·domain:ui·area:access·pm:queue. Accepted as the close-out of the "affordance ≠ grant" family; #12081 items 1, 5 and 6 fold in hereTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T03:53Z. ⛔ Not a claim, ⛔ not a dispatch.Re-read on objectui
main1b2d0160fe:plugin-form/src/fieldWriteGate.ts:138disables a field whenevermode !== 'view'andcheckField(object, field, 'write')fails, so create mode is included. The defect holds onmain, not only in the 17.7.0 bundle.Why p1: a role designed as create-only cannot create anything through the form. There is no workaround, and the server accepts the same create.
The family: each console affordance must read the grant it actually exercises. One pass of one app found four members:
allowCreate: false;The earlier members closed one at a time: Row kebab offers Edit/Delete on records the server refuses — the list ANDs only the OBJECT-level grant, the detail header also folds the RECORD-level one #4296, [repo:objectui] Edit affordance is derived from the object-level writeScope only — a user holding a record-level
editshare (sys_record_share) sees no Edit button while PATCH on the same record succeeds #10107, forms: when the object's create affordance is closed, every field is disabled with no explanation, and the wizard's Next stays enabled #11000, console: the record Attachments panel offers Upload and delete to a caller whose grant cannot attach or delete, so the upload fails at the last step #12047.Direction:
Enumeration pin: iterate every affordance in the map against four grant shapes: create-only, edit-only, read-only and full. Each affordance shows exactly when its grant allows it. An affordance added later without a map row turns it red.
Each item is measured on
mainfirst, since the report is from 17.7.0.objectstack-fleet commentedon Oct 10, 2026
Claim: PM loop round 3
Session:
session_01CGZy1BGCjdN5cXqL9cnvB8Account:
os-support-aiBranch:
claude/issue-12082-affordance-grant-mapWorktree:
objectui-issue-12082Domain:
domain:uiSeat:
domain:ui#3Scope: this card plus objectui#12081 items 1, 5 and 6, which triage folded in here (
6093495104,6093502833). Items 2, 3, 4, 7 and 8 stay on objectui#12081.File surface (line numbers on
main1b2d0160):resolveEffectiveCrudAffordances(packages/core/src/utils/managedBy.ts:177) or inpackages/permissions/src/. ⛔ One copy, ⛔ no per-component permission logic.packages/plugin-form/src/fieldWriteGate.tsgateByPermission(:138) askscheckField(…, 'write')whenevermode !== 'view'. A create form asks the create question instead. The resolver side lives inpackages/permissions/src/(MePermissionsProvider.tsxcheckField:302;PermissionProvider.tsx; thePermissionContext.tstype). The create answer follows what the server enforces on insert, measured first. ⛔ No client rule the server does not have.packages/app-shell/src/views/RecordDetailView.tsxresolveRecordHeaderActionGates(:316);ObjectView.tsx(:2062) andObjectDataPage.tsx(:432);packages/fields/src/widgets/LookupField.tsxcanCreate(:1160);RelatedRecordActionsBridge.tsx:375,plugin-listListView.tsx:1883andplugin-gridrowCrudAffordances.ts:238. Each is named in the PR..changeset/12082-*.mdper touched package, or one file naming all of them.⛔ Not on it:
/me/permissionsenvelope change (objectstack's lane);fieldWriteGate.tsdocblock), which stays as is;packages/components/src/ui/**.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectuiover these paths: no path-derived mandate; default tier). The contract review runs at the ceiling,CONTRACT_REVIEW_TIER, because of the line below.Clause-②: yes
Why
Clause-②: yes: the map is a new exported surface read across packages (coreorpermissions, thenapp-shell,fields,plugin-form), and asking the create question may widencheckField's published action union. Changesets areminorwhere a published surface widens andpatchwhere only behaviour moves. ⛔ Nevermajor(objectui AGENTS.md §9).Responsibility:
objectui console: each affordance decides on its own which grant it reads, so a create form gates its fields on edit and a create-only role cannot create, the header hides Edit from a caller whose update is allowed, and New / Import / lookup "Create new" show to a caller who cannot create | the platform path: one affordance-to-grant map that every affordance reads, with create-mode fields following the server's insert rule | every user of a create-only, edit-only or read-only grant in any metadata appThread-read: 6093495104
Serial constraints cleared:
noneblocking.types,collaboration,data-objectstack,plugin-designerandproviders, none of these files. Dependabot PRs build(deps): Bump next from 16.3.6 to 16.3.8 in /apps/site in the npm_and_yarn group across 1 directory #11844 and chore(deps): bump next from 16.3.6 to 16.3.8 #11845 and the release PR chore: release packages (next) #11600 touch none of them either.pm:dispatchedfor batch 10, which is in the merge queue with no dev in flight. Serial dispatch: this card's dev is the seat's one subagent.editshare (sys_record_share) sees no Edit button while PATCH on the same record succeeds #10107, forms: when the object's create affordance is closed, every field is disabled with no explanation, and the wizard's Next stays enabled #11000 and console: the record Attachments panel offers Upload and delete to a caller whose grant cannot attach or delete, so the upload fails at the last step #12047 are all closed. Their pins stay green.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{ "issue": 12082, "status": "done", "branch": "claude/issue-12082-affordance-grant-map", "pr": "https://github.com/objectstack-ai/objectui/pull/12084", "session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent mode: the parent seat's id)", "premise_still_valid": true, "summary": "One affordance-to-grant map now lives in @object-ui/core (AFFORDANCE_GRANTS + resolveAffordance / resolveFieldAffordance / formFieldsAffordance): 18 rows, each naming the CRUD-affordance bit, the object grant and (for field rows) the field question; the verdict is managed-object policy AND effective API operations (getObjectApiOperations, kept inside the map) AND the caller's grant, predicates only when allowed. Core was chosen over permissions because every reader already depends on both and permissions depends on types alone (homing it there adds a permissions-to-core edge; in core the principal is structural, no edge either). The p1 card: checkField gained 'create' (explicit entry, else allowCreate) = the server's insert rule read in objectstack main 76bc1e03 (plugin-security step '2.5. Field-Level Security write enforcement' via computeForbiddenFieldWrites -> FieldMasker.detectForbiddenWrites/getNonEditableFields on insert and update alike; PermissionEvaluator.getFieldPermissions builds entries only for named fields; OPERATION_TO_PERMISSION insert -> allowCreate); every ObjectForm layout's render gate, outbound filter and form-wide lock read the form's row, and a create-mode MasterDetailForm's line cells ask the create question of the child. Measured on main 1b2d0160f: the p1 pin was red on all 9 layouts with body {} (24 failed / 9 passed), now 35/35. Item 1 does not reproduce from objectui's side: for hotclm's clm_payment_plan (controlled_by_parent, no managedBy/userActions/apiMethods) the only principal-dependent header input is the explain record verdict; objectstack-ai/objectstack#22529 (merged 2026-10-09, not an ancestor of the 17.7.0 tag commit 4e4e8814, control leg exit 0) made explain's update verdict on controlled_by_parent records come from the master-detail write check - its own table shows allowed:true + record.visible:false + PATCH 200, the hotclm shape; the header now also reads the update grant (pinned). Item 5 does not reproduce: both list surfaces already ANDed can(create) on main and in 17.7.0, and hotclm binds every position to clm_requester (src/security/bind-position-sets.ts), which grants allowCreate on clm_contract, so the records manager's effective grant allows create; pinned through the map anyway. Item 6 reproduced on main (3 red / 2 green against main's LookupField) and is fixed: Create new reads the target's lookupCreateNew row. Census readers converted: RecordDetailView, ObjectView, ObjectDataPage, importTargetFields (now the create question), RelatedRecordActionsBridge, RecordAttachmentsPanel, ListView, ObjectGrid + rowCrudAffordances (now takes the map's verdicts), ImportWizard, DetailView (operation set + grant; the object's own policy stays the host's channel per objectui#4419), record:details (now reads the update grant), console ProfilePage. Out of family: managedByEmptyState (empty-state copy from the bucket, shows/hides nothing), useFieldPermissions canWrite/writableFields (the resolver's own API), read gates. Known gap, outside the brief's grep population because they read NO grant: plugin-view ObjectView create button, plugin-calendar quick-create and drag-to-reschedule, plugin-kanban card move, LineItemsPanel add/remove lines - see out_of_scope_findings and open_questions.", "tests": "All at branch head 62413c986 unless noted (package suites ran at 9104511cc; later commits touched only docs, the changeset and 3 pin files, re-run green at fc8e0193f and 62413c986). Package suites (vitest from repo root, under os-verify-lock): core+permissions 209 files / 4075 passed 27 skipped; fields 244 passed 1 skipped / 3973 passed; plugin-list 125 / 1384; plugin-grid 43+154 = 197 files / 373+1478 passed; plugin-detail 156+94 files (1 skipped) / 1397+1036 passed; plugin-form 171 / 2069 passed 1 skipped; app-shell scoped to the touched views (59+60+85 = 204 files, 416+560+964 passed); console scoped ProfilePage 3 files / 29 passed; scripts/__tests__ 179 passed 2 skipped / 5452 passed. Pins at 62413c986: createFormGrant-12082 35/35, affordanceGrantMap-12082 27/27 (+ check-hand-rolled-comment-mask 22/22), LookupField.createGrant-12082 5/5; family pins green: closedAffordanceNotice-11000, RecordAttachmentsPanel.affordanceGrant-12047, useRecordEditable.principalScope (10107), rowRecordCrudVerdict + rowCrudAffordances + rowCrudEffectiveOps (4296) - 9-file final pin run 161/161. Measured on main first: p1 pin vs main 24 failed / 9 passed (every field locked, create body {}); item-6 pin vs main's LookupField 3 failed / 2 passed (controls green), restored blob-equal. Ablations (scripts/ablation-replace.mjs, WRAP mode, predictions first, every restore blob == HEAD and git diff HEAD empty; vitest aliases @object-ui/core to src, so no dist leg): A1 createFormFields field 'create' -> 'write' (put 'write' back): predicted 16+3 red, observed 19 failed / 43 passed; A3 plant can(referenceTo, 'create') in LookupField: census red naming packages/fields/src/widgets/LookupField.tsx, 1 failed / 26 passed as predicted; A4 recordEdit grant 'update' -> 'create': 4 failed / 38 passed as predicted (row table, create-only and edit-only shapes, header create-only row); A5 MasterDetailForm formMode forced 'edit': 1 failed / 34 passed as predicted. Reverse type check against REBUILT .d.ts: a throwaway probe in plugin-form refused resolveAffordance('noSuchAffordance') with TS2345 listing the map's rows, accepted checkField(o, f, 'create') and refused 'bogus' with TS2345 naming 'read' | 'write' | 'create'; probe deleted. Eager closure: base 1b2d0160f 3,244,032 B gz vs head 3,244,687 B gz (+655 B gz, +1,711 B raw, 290/290 eager chunks), check:eager-closure exit 0 at head (3168.6 KB, budget 3204.6 KB).", "mcp_calls": "0 GitHub MCP calls. 1 session tool call: mcp__claude-code-remote__add_repo (objectstack-ai/hotclm, access read) - answered read_available, attached nothing; used for an anonymous shallow clone into the scratchpad to read hotclm's metadata for items 1 and 5.", "api_writes": "3 REST writes, each one fleet-write relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create -> POST /repos/objectstack-ai/objectui/pulls (#12084, draft; read-back 7694 B identical); label-write --assign os-support-ai -> POST /repos/objectstack-ai/objectui/issues/12084/assignees (read-back matches); this os-dev-report -> POST /repos/objectstack-ai/objectui/issues/12082/comments. Plus git push to claude/issue-12082-affordance-grant-map (not REST).", "open_questions": [ { "question": "Five write affordances read NO grant at all, so the brief's grep census (grant reads) cannot see them: plugin-view ObjectView renderToolbar showCreateButton (create), plugin-calendar ObjectCalendar handleDateClickDefault / handleTimeRangeSelectDefault quick-create (create) and handleEventDropDefault drag-to-reschedule (update), plugin-kanban ObjectKanban handleCardMove (update), plugin-form LineItemsPanel add / remove lines (create / delete on the child; only schema.readonly). They are this family's members; how are they closed?", "options": [ "A - a patch round on this claim: give each a map row (or reuse listNew / rowEdit / rowDelete) and read it; claim's file surface amended to plugin-view, plugin-calendar, plugin-kanban, LineItemsPanel", "B - a follow-up dispatch on this card after #12084 lands, keeping this PR's size as is", "C - leave them: the server refuses the write anyway" ], "recommendation": "B, because each surface needs its own runtime pin (calendar and kanban are heavy suites) and #12084 already spans 9 packages; C contradicts the Zone 1 ruling that every affordance reads the map. Note the census pin cannot catch this shape by construction - it refuses grant reads outside the map, not affordances with no grant read - which the follow-up should state rather than imply." } ], "out_of_scope_findings": [ "class: a (same family, folds into this close-out card - no separate card) - reach: NOT MEASURED at a public door, source read only on objectui main 1b2d0160f - evidence: plugin-view ObjectView renderToolbar `showCreateButton = schema.showCreate !== false && operations.create !== false`; plugin-calendar ObjectCalendar handleDateClickDefault / handleTimeRangeSelectDefault open quick-create on calendarConfig + objectName + dataSource.create only, handleEventDropDefault persists via dataSource.update; plugin-kanban ObjectKanban handleCardMove writes the group field with no grant read; LineItemsPanel add / remove lines gate on schema.readonly only - dedupe words: quick-create grant, drag reschedule permission, kanban card move permission, object-view create button permission, line items add remove grant", "carrier: hotclm seat (repo:hotclm), noted not filed - objectui#12081 item 1's cause is the explain record verdict for controlled_by_parent rows, fixed upstream by objectstack-ai/objectstack#22529 after the 17.7.0 tag; hotclm's finance header Edit should be re-measured once its @objectstack pin includes that merge", "carrier: hotclm seat, noted not filed - objectui#12081 item 5 reads one permission set; the records manager's effective grant on clm_contract includes clm_requester's allowCreate (every position binds clm_requester), so New / Import are correct for that user", "carrier: 承接者:无 - noted not filed - MePermissionsProvider.check keys objects by the name as given while checkField and getObjectApiOperations lowercase it; every reader passes the object's own lowercase name, no divergence measured", "carrier: 承接者:无 - noted not filed - LookupField reads `allow_create ?? allowCreate` (two spellings of one key), a consumer-side alias outside this card", "carrier: 承接者:无 - noted not filed - ObjectGrid's comment above rowActionDefsList carries a pre-existing cross-file line address (index.tsx plus a line number); not swept here per AGENTS.md #11" ], "gates": [ { "command": "turbo run build --filter=@object-ui/app-shell... (and console^..., and cli/plugin-ai/gantt/map/markdown/timeline closures) --concurrency=2", "exit": 0, "verdict": "Tasks: 29/29, 34/34, 20/20 successful" }, { "command": "pnpm --filter @object-ui/{core,permissions,plugin-form,fields,plugin-list,plugin-grid,plugin-detail,app-shell} type-check", "exit": 0, "verdict": "each echoed its own type-check script, 0 error TS (tsc --noEmit && tsc -p tsconfig.test.json)" }, { "command": "pnpm --filter @object-ui/console type-check", "exit": 0, "verdict": "> @object-ui/console@17.7.0 type-check, no error" }, { "command": "pnpm exec vitest run (package suites listed under tests)", "exit": 0, "verdict": "all Test Files passed (counts in tests)" }, { "command": "pnpm exec eslint (39 touched .ts/.tsx)", "exit": 0, "verdict": "0 errors; warnings on added lines: no-explicit-any in new test files and a pre-existing react-hooks/preserve-manual-memoization that moved with LookupField's deps line (9 before, 9 after)" }, { "command": "pnpm check:control-bytes", "exit": 0, "verdict": "check-control-bytes: OK (scanned 8277 tracked text file(s))" }, { "command": "pnpm check:test-path-roots", "exit": 0, "verdict": "check-test-path-roots: OK" }, { "command": "pnpm check:changeset-claims", "exit": 0, "verdict": "No pending changeset names a file this change touches." }, { "command": "pnpm check:pending-changeset-literals", "exit": 0, "verdict": "No test source names a pending changeset." }, { "command": "node scripts/check-changeset-presence.mjs", "exit": 0, "verdict": "39 source file(s) of 9 released package(s) changed, and this change declares 1 changeset(s)" }, { "command": "pnpm check:new-line-citations", "exit": 0, "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0" }, { "command": "pnpm check:readme-exports", "exit": 0, "verdict": "check-readme-exports: OK (585 of 870 import bindings judged, 0 fabricated); first run exit 1 = 6 unbuilt packages, rebuilt and re-run" }, { "command": "pnpm check:doc-fences / doc-types / doc-snippets / doc-examples", "exit": 0, "verdict": "each passed" }, { "command": "pnpm check:vi-mock-specifiers / vi-mock-inherit / vi-mock-override-shape", "exit": 0, "verdict": "each passed" }, { "command": "pnpm check:phantom-deps / unused-deps / self-import / esm-specifiers / unreferenced-sources", "exit": 0, "verdict": "Every in-scope import is declared by the package that publishes it (phantom-deps); the rest passed" }, { "command": "pnpm check:comment-mask-corpus", "exit": 0, "verdict": "1 file(s) disagree, within the residue objectui#7882 is holding open" }, { "command": "pnpm check:eager-closure (after apps/console vite build)", "exit": 0, "verdict": "Console eager closure is 3168.6 KB gzipped across 290 of 2474 chunks (budget: 3204.6 KB, headroom: 35.9 KB)" }, { "command": "pnpm exec vitest run scripts/__tests__/", "exit": 0, "verdict": "179 passed, 2 skipped" }, { "command": "CI on #12084", "exit": null, "verdict": "in_progress at 62413c986; at fc8e0193f Test (shard 4/8) failed on check-hand-rolled-comment-mask (census used a private comment regex) - fixed in 62413c986, the other shards were cancelled by that push" }, { "command": "i18n gates (check:i18n-keys / drift / dead-keys / designer-parity)", "exit": null, "verdict": "not run: no locale pack changed (no new keys; the notice copy already existed)" } ], "deviations": [ "The PR body was written once, when the draft opened after the first commit (os-dev rule: no PATCH by dev); it predates the gate readout, the ablations, the eager delta, the final reader list and the fix for check-hand-rolled-comment-mask. Requested amendment for the seat: replace the Readers section with the converted list in summary plus the out-of-family list (managedByEmptyState, useFieldPermissions, read gates), add the MasterDetailForm line-cell change, the tests/ablation/eager readings in tests, and the no-grant family members from open_questions under Acceptance notes.", "Files beyond the claim's named surface, each a census reader the claim says to name in the PR: plugin-detail DetailView + record-details, plugin-grid ImportWizard + rowCrudAffordances, apps/console ProfilePage (+ its 3 test stubs), the core/permissions READMEs and content/docs/plugins/plugin-form.mdx (AGENTS.md #2). MasterDetailForm (create-mode line cells ask the create question) was taken under the bounded in-place rule (same defect class, mechanical, pinned by createFormGrant-12082 + ablation A5, no other claim on the file); the claim's file surface needs that addition.", "Behaviour moves beyond the card's rows, all toward the server's refusal: the grid add-record row now also honours managed-object policy and the effective create operation (gridAddRow row); DetailView's object gate adds the effective operation set; record:details in-place editing now reads the update grant; the form-wide lock and its notice now also engage on a denied grant for the form's mode.", "The census pin is a test-level guard the card and the claim rule for (an affordance or reader outside the map turns it red); no new check:* gate or workflow was added.", "My hand-derived gate list missed scripts/__tests__/check-hand-rolled-comment-mask.test.ts (a root-level test that scans package tests); CI shard 4 caught it and it is fixed. The root scripts suite was then run locally in full.", "Read access to objectstack-ai/hotclm via add_repo (read_available, nothing attached) and an anonymous shallow clone in the scratchpad, to measure items 1 and 5 against hotclm's metadata. A temporary ref refs/os-dev-12082/main was written to the shared .git to read origin/main's delta and deleted after.", "Commit trailers are the model-free pair per objectui AGENTS.md (Claude-Session + Co-authored-by: Claude), not the harness's model-named form." ], "files_changed": [ ".changeset/12082-affordance-grant-map.md", "apps/console/src/pages/system/ProfilePage.tsx", "apps/console/src/pages/system/__tests__/ProfilePage.access.test.tsx", "apps/console/src/pages/system/__tests__/ProfilePage.language.test.tsx", "apps/console/src/pages/system/__tests__/ProfilePage.sharedSelect-11865.test.tsx", "content/docs/plugins/plugin-form.mdx", "packages/app-shell/src/views/ObjectDataPage.tsx", "packages/app-shell/src/views/ObjectView.tsx", "packages/app-shell/src/views/RecordAttachmentsPanel.tsx", "packages/app-shell/src/views/RecordDetailView.headerActionGates.test.tsx", "packages/app-shell/src/views/RecordDetailView.tsx", "packages/app-shell/src/views/RelatedRecordActionsBridge.tsx", "packages/app-shell/src/views/importTargetFields.test.ts", "packages/app-shell/src/views/importTargetFields.ts", "packages/core/README.md", "packages/core/src/index.ts", "packages/core/src/utils/affordanceGrants.ts", "packages/fields/src/widgets/LookupField.createGrant-12082.test.tsx", "packages/fields/src/widgets/LookupField.tsx", "packages/permissions/README.md", "packages/permissions/src/MePermissionsProvider.tsx", "packages/permissions/src/PermissionContext.ts", "packages/permissions/src/PermissionProvider.tsx", "packages/plugin-detail/src/DetailView.tsx", "packages/plugin-detail/src/renderers/record-details.tsx", "packages/plugin-form/src/DrawerForm.tsx", "packages/plugin-form/src/MasterDetailForm.tsx", "packages/plugin-form/src/ModalForm.tsx", "packages/plugin-form/src/ObjectForm.effectiveOps.test.tsx", "packages/plugin-form/src/ObjectForm.tsx", "packages/plugin-form/src/SplitForm.tsx", "packages/plugin-form/src/TabbedForm.tsx", "packages/plugin-form/src/WizardForm.tsx", "packages/plugin-form/src/affordanceGrantMap-12082.test.tsx", "packages/plugin-form/src/createFormGrant-12082.test.tsx", "packages/plugin-form/src/fieldWriteGate.ts", "packages/plugin-form/src/formChrome.i18n-11071.test.tsx", "packages/plugin-form/src/sanitize.ts", "packages/plugin-grid/src/ImportWizard.tsx", "packages/plugin-grid/src/ObjectGrid.tsx", "packages/plugin-grid/src/__tests__/rowCrudAffordances.test.ts", "packages/plugin-grid/src/rowCrudAffordances.ts", "packages/plugin-list/src/ListView.tsx" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Claim amendment and seat answer —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-10T06:43ZThis amends claim
6093729821after the dev's report6094735932(status: done, PR objectui#12084 at62413c98).1. The dev's open question: five write affordances read no grant at all
These five sit outside the brief's grep census, which looked for grant reads:
ObjectViewcreate button;LineItemsPaneladd / remove lines.Seat answer: B. They are closed by a follow-up dispatch on this card, after objectui#12084 lands. This is a sequencing call, so it is in the class the seat answers without escalating.
Consequences:
Fixes #12082toPart of #12082, so the card stays open for the remainder.pm:queuewithpm:retriage, and the remaining scope written on the card. The p1 defect is closed by objectui#12084, so triage re-grades what is left.listNew,rowEditorrowDelete), and its reach is measured first. The census pin as it stands refuses grant reads outside the map; it cannot see an affordance that reads no grant. The follow-up must state that limit or close it. It must not imply the pin covers that shape.2. File surface: added, as census readers the claim said to name
packages/plugin-detail/src/DetailView.tsxandsrc/renderers/record-details.tsx;packages/plugin-grid/src/ImportWizard.tsxandsrc/rowCrudAffordances.ts;apps/console/src/pages/system/ProfilePage.tsxand its three test stubs;packages/plugin-form/src/MasterDetailForm.tsx: a create-mode form's line cells ask the create question of the child. Same defect class, pinned bycreateFormGrant-12082and ablation A5;coreandpermissionsREADMEs andcontent/docs/plugins/plugin-form.mdx(objectui AGENTS.md Add automated testing infrastructure and CI/CD workflows #2).Clause-②: yesstands.@object-ui/core(the map) and@object-ui/permissions(checkFieldaccepts'create') areminor. The rest arepatch.3. objectui#12081 items 1, 5 and 6
controlled_by_parentrows, fixed upstream by 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.clm_requester'sallowCreate.mainand fixed.The seat records these readings on objectui#12081 at landing.
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
ACCEPT —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-10T07:01Z. PR objectui#12084, head62413c98.PR shape:
main, based on1b2d0160.git merge-treeagainstmain12ff2563(objectui#6349 batch 10 landed since) is clean.Part of #12082, per the seat's answer B (6094765692): no closing keyword in the body or in any of the ten commits, so this card stays open for its remainder.Clause-②: yesat line start.os-support-ai, set with no refusal.Scope: 43 files, +1714/−284, on the claim
6093729821as amended in6094765692(census readers inplugin-detail,plugin-grid, the consoleProfilePage, andMasterDetailForm's create-mode line cells). The path fork reads no governed path, and the change is under the 5,000-line human-merge threshold.What lands:
checkField(object, field, 'create')answers from the explicit field entry when there is one, and fromallowCreateotherwise. That follows the server's insert rule, read at objectstack76bc1e03: field step 2.5 refuses only an expliciteditable: false, and object admission maps insert toallowCreate. Measured onmainfirst: 24 red / 9 green across all nine layouts, with the create body{}. At the head: 35 / 35.AFFORDANCE_GRANTSin@object-ui/core, with 18 rows.resolveAffordancereturns managed-object policy ∧ effective API operation set ∧ the caller's grant, and surfaces predicates only when allowed.resolveFieldAffordanceandformFieldsAffordancecover the form rows. Every census reader now reads it, and the PR names the out-of-family readers with their reasons.main(3 red) and fixed through thelookupCreateNewrow, asked of the target object.@object-ui/coreand@object-ui/permissionsminor, seven packagespatch. Fail-open with no provider is unchanged.Contract review:
6094907310on the PR, rendered atCONTRACT_REVIEW_TIERby an isolated, read-only reviewer on this head, reads PASS. The seat checked the record's lines:Served-tier: CONTRACT_REVIEW_TIER;Head-shaequal to the head;Local-runs: none;Implemented-byandReviewed-bysigned;VERDICT: PASS;It judged:
PermissionContextValueare not exported from their package entries, so the widenedcheckFieldunion is purely additive; every console object form setsmode, somode === 'create'reaches every create surface;minor/patchmatching what the diff publishes;Gates:
6094735932). The four ablations went red as predicted, with restores blob-equal.check:eager-closurepasses.62413c98: 43 runs, 40 success, 3 skipped (the expected three), 0 failure;mergeable_stateclean.Process note, acknowledged here: the dev wrote a temporary ref,
refs/os-dev-12082/main, into the shared checkout's.gitto readorigin/main's delta, then deleted it. That is a write to a shared checkout, which the brief forbids. It has no effect on the diff or the head. The reviewer raised it, and the seat records it rather than letting it pass silently.Out-of-scope findings, one line each:
check's object-name casing: Acceptance notes;LookupField'sallow_create ?? allowCreate: Acceptance notes;ObjectGrid's old cross-file line address: dropped (pre-existing, AGENTS.md [WIP] Update documentation for project #11).Landing: ready + auto-merge through the queue, now. Its merge does not close objectui#12082. At landing, the seat releases the claim to
pm:queuewithpm:retriage, writes the remaining five affordances on the card, and records items 1, 5 and 6 on objectui#12081.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Landed: PR objectui#12084 →
main023f00d4· the p1 defect is fixed; the card stays open for its remainder and goes back to triagedomain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-10T07:21Z.12ff2563, an ancestor oforigin/main.git merge-treeof that parent and the accepted head62413c98(treebc141bfdon both).AFFORDANCE_GRANTSis inpackages/core/src/utils/affordanceGrants.ts:110and exported from@object-ui/core's entry.MePermissionsProvider'scheckFieldanswers'create'fromallowCreatewhen no field entry exists (:335).LookupFieldreadsresolveAffordance('lookupCreateNew', …)(:1170).12ff2563the map file is absent andcheckField's union has no'create'.6094907310PASS; ACCEPT6094918906.Release:
session_01CGZy1BGCjdN5cXqL9cnvB8(accountos-support-ai) releases claim6093729821, as amended in6094765692.Part of #12082, per the seat's answer B. The maintainer has also told this seat to clock out once this merge lands, verbatim, 2026-10-10: 「当前任务处理完,合并后就下班。」 So no follow-up is dispatched from this seat.pm:dispatchedand the assignee are removed in this act. The card goes back topm:queue, andpm:retriageis added for the question below.What remains on this card (the follow-up): five write affordances read no grant at all, so the first round's census (grant reads) could not see them:
ObjectView's create button (showCreateButton): create;handleDateClickDefault,handleTimeRangeSelectDefault): create;handleEventDropDefault): update;handleCardMove): update;LineItemsPaneladd / remove lines: create / delete on the child.For each: measure reach on
mainfirst, then give it a row in the map (or reuselistNew,rowEditorrowDelete) and read it throughresolveAffordance. The census pin as it stands refuses grant reads outside the map; it cannot see an affordance that reads no grant. The follow-up states that limit or closes it.pm:retriage— what this seat asks of triage: re-grade the remainder.priority:p1was for the create form, which a create-only role could not use at all, and that is fixed onmain. The five above show an affordance whose write the server then refuses, which is the family's p2 shape (objectui#12081's triage). Say whether the remainder stays on this card or moves to a successor card.Generated by Claude Code
47 remaining items
objectstack-fleet commentedon Oct 16, 2026
Unlock scan:
pm:blocked→pm:queue· the epic step this card waited on has landed · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T10:47ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.
6110279954) waited on epic objectui#7611's C9 part 2, "which owns those files" (seat 2's landing note6109930881). All of C9 has landed since then:6111491126, 2026-10-11T17:04Z).faf4ce9523) is the last change toAssignedUsersSection.tsx.6117462399), closedcompletedon 2026-10-12.packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsxorPositionHoldersSection.tsx(read now). feat: Setup catalog pages read the registry (editable undersingle, read-only under a wall); assignment pages stay data pages; pickers list the registry (objectstack ADR-0131 D3/D7) #7611 stayspm:on-holdon feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) objectstack#15196, a server-side epic that names no objectui file.main8a8bcf3dde,packages/plugin-form/src/affordanceGrantMap-12082.test.tsx:683–:690still holds exactly the fourunmappedSetup assignment entries (permission-set and position, create and delete).WRITE_SITEShas nounmappedentry.6109740908still applies.objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 2 (the four Setup assignment entries, the card's last
unmappedcensus entries; triage6213818308and6102063887; reviewer note (e) of6109740908)Session:
session_01Vk15bz8xHtKGUfXfCm16h4Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12082-setup-assignment-censusWorktree:
objectui-issue-12082Domain:
domain:uiSeat:
domain:ui#3File surface: the census pin
packages/plugin-form/src/affordanceGrantMap-12082.test.tsx(the four Setupunmappedentries becomemappedoroutOfFamily, plus the census's own bookkeeping);packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsxandPositionHoldersSection.tsx(only where a site that a grant-less user can reach gets its affordance gated);packages/core/src/utils/affordanceGrants.ts(a row only where none fits); the record feed pin besidepackages/app-shell/src/views/RecordDetailView.tsx(reviewer note (e): one case with the registeredsys_commentandsys_comment_reactionschemas inobjects); tests beside each; the docs that name the map's rows; and one.changeset/12082-*.md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(default tier; permission-affordance semantics with a reach measurement per site;dispatch-gates --tier --repo objectstack-ai/objectuiover the named paths: no path-derived mandate)Clause-②: no (a prediction, as in claim
6108058067: if a row is added toAFFORDANCE_GRANTS, the published value widens, the report says so, this line is corrected toyes, and the review runs atCONTRACT_REVIEW_TIERas6109740908did)Responsibility:
@object-ui/app-shell's Setup permission-set and position assignment writesys_user_permission_set/sys_user_positionrows and read no grant on those objects |AFFORDANCE_GRANTSandresolveAffordancein@object-ui/core, and theWRITE_SITEScensus; Setup's own route gate may already be the boundary (triage's likely case) | a user who reaches Setup's assignment sections without the object grant; reach is measured per site firstThread-read: 6213818308
Serial constraints cleared: no in-flight lane claim and no open objectui PR names
AssignedUsersSection.tsx,PositionHoldersSection.tsx,affordanceGrants.ts,affordanceGrantMap-12082.test.tsxorRecordDetailView.tsx(read 2026-10-16T12:08Z). objectui#12406 (seat 1) holdsmetadata-admin/PermissionAdvancedFacets.tsxandmetadata-admin/i18n.ts; this card touches thati18n.tsonly for a new string, and if it must, whichever lands later mergesmain. objectui#12606 (seat 4) holdsmetadata-admin/ResourceListPage.tsxonly.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{ "issue": 12082, "status": "done", "branch": "claude/issue-12082-setup-assignment-census", "pr": "https://github.com/objectstack-ai/objectui/pull/12685", "session": "session_01Vk15bz8xHtKGUfXfCm16h4 (subagent mode: the dispatching seat's id; the relay resolved the same id)", "premise_still_valid": true, "summary": "The census's last four unmapped WRITE_SITES entries (Setup's permission-set holders addUsers/removeUser on sys_user_permission_set, position holders addHolders/removeHolder on sys_user_position) were graded on measured reach. All four are reachable without the grant, which falsifies triage's likely case: Setup's own gate is not the boundary. The Setup app asks setup.access, its Positions and Permission Sets nav entries ask nothing more, and objectstack's shipped organization_admin set (plus its wall-less derivation) holds setup.access while it is read-only on both assignment objects; the server's CRUD step refuses that caller's write. So each section now reads two EXISTING rows asked of its assignment object: Add reads relatedNew, Remove reads relatedRowDelete (the rows a related list's Add existing and row Delete read). A closed row withholds the Add button, its picker and each row's Remove; the holders still list; no provider reads open. The census grades all four mapped and UNMAPPED_BACKLOG is empty (the card's done-when), so the PR body's first line carries the closing keyword for this card. No AFFORDANCE_GRANTS row is added, so the claim's Clause-2 'no' holds as measured. Reviewer note (e): the feed pin gains one case with the registered sys_comment and sys_comment_reaction schemas in the page objects.", "tests": "Final head 7e9eed3 (merge of origin/main dcd1041 into 4754b38). Locked run: pnpm exec vitest run --maxWorkers=2 over 43 files (census pin; new setupHoldersGrant-12082.test.tsx; AssignedUsersSection, AssignedUsersSection.contractEnvelope-5945 and PositionHoldersSection suites; every PermissionMatrixEditor.* suite; ResourceEditPage.setupCatalog-7611, ResourceEditPage.tabDeepLink-12496, trailDoorsGate-12475, walledCatalogReadOnly-12183, capabilityGateChannel-7234, ResourceListPage.environmentScope-7611, PermissionAdvancedFacets.adminScopeAnchor; RecordDetailView.feedGrant-12082 and feedCommentEditDelete-12268; useApproverDirectory.contractEnvelope-5945; the seven app-shell ratchet suites) → 'Test Files 43 passed (43)', 'Tests 373 passed (373)', TESTS4_EXIT=0. The same 42-file set (without the merged PermissionAdvancedFacets suite) at 4754b38: 42 passed, 361 tests. Ablations at 4754b38, one-time, through objectstack scripts/ablation-replace.mjs wrap mode (anchor x1 → x0, blob changed; restore proved: blob == HEAD and git diff HEAD empty; tree porcelain 0 after): A1 AssignedUsersSection canAdd → true: red, 'Tests 5 failed | 58 passed (63)' (4 section cases: org admin, read-only, delete-only, operation set; plus the census 'every mapped site names rows of the map, of its own verb, each read by one of the files it names'). A2 PositionHoldersSection canRemove → true: red, 'Tests 5 failed | 58 passed (63)' (org admin, read-only, create-only, operation set; same census assertion). A3 note (e): the registered sys_comment schema given userActions create false: red, 'Tests 1 failed | 18 passed (19)', only the new case, on expect(composer(view)).not.toBeNull(). The first A3 attempt was a no-op, void: the replacement carried its own anchor, so the tool refused (anchor 1 → 1) and restored; re-run with an anchor-altering replacement. Direction observed in all three: red (the common case). Subjects resolve through relative imports and vitest src aliases, so no dist preflight applies.", "gates": "Local subset, objectui (hand-derived; objectui has no dispatch-gates.mjs). At 7e9eed3: turbo run build --filter='@object-ui/app-shell...' --concurrency=2 BUILD4_EXIT=0 ('Tasks: 29 successful, 29 total'); pnpm --filter @object-ui/app-shell type-check TC_APP4_EXIT=0 and pnpm --filter @object-ui/plugin-form type-check TC_FORM4_EXIT=0 (both echo 'tsc --noEmit && tsc -p tsconfig.test.json'; tsconfig.test.json includes src/**/*.test.tsx); node scripts/check-changeset-presence.mjs exit 0 ('5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'); pnpm check:new-line-citations exit 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)'); raw control-byte grep of the 8 changed files: none (grep exit 1). eslint on the 5 changed source/test files: exit 0, 0 errors, 30 warnings, one on an added line (the pre-existing onSelectRecords any, re-indented under canAdd); run at 4754b38, files byte-identical at 7e9eed3. pnpm check:readme-exports: exit 1, PARTIAL: 72 self-imports could not be judged, all in packages outside the built closure (cli, plugin-ai, plugin-gantt, plugin-map, plugin-markdown, plugin-timeline); zero findings name packages/core/README.md or packages/app-shell/README.md; whole-tree verdict is CI's README Export Check. CI at 7e9eed3, read once at PR open (not waited on): 35 check-runs, 7 success (Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, label), 3 skipped by design (Test (coverage shard ...), Test (coverage), dependabot), 25 in_progress (Lint, Type Check, Repo Gates, Build & E2E, Build Docs, Bundle Analysis, Changeset Claim Re-read, Test (dist pins), Test (shard 1..16/16), Live E2E (informational)). NOT MEASURED: the remaining 467 metadata-admin suites and the rest of the farm (CI's).", "mcp_calls": "0", "api_writes": "2 relay strokes = 2 x POST /repos/objectstack-ai/objectstack/dispatches (fleet-write), executing 3 REST writes on objectstack-ai/objectui as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectui/pulls (draft, PR 12685), POST /repos/objectstack-ai/objectui/issues/12685/assignees (os-elon-musk), POST /repos/objectstack-ai/objectui/issues/12082/comments (this report). Plus git push x3 of the branch (probe, 4754b38, 7e9eed3), not REST. No label write, no card assignee or label write.", "open_questions": [], "out_of_scope_findings": [ "class: b · reach: exception: release-text (published in @objectstack/spec 18.0.0-next.2 dist/migrations, which objectui's install carries) · evidence: objectstack's semantic migration entry walled-catalog-tenant-writes-retired says in its replacement text that 'an organization's administrators assign it to their own members through sys_user_position and sys_user_permission_set, which are unchanged', while the shipped organization_admin set and its derived organization_admin_no_bypass are read-only on both objects (objectstack pin 'organization_admin is read-only on global RBAC tables to prevent privilege escalation'), and the security middleware's step 2 CRUD check refuses that write. One of the two declarations is wrong, and which one is an objectstack product call (anti-escalation rule versus the ADR-0131 D3 assignment promise). Seam: spec:migrations entry walled-catalog-tenant-writes-retired (replacement) → runtime: plugin-security default-permission-sets organization_admin objects entries / security-plugin CRUD step · dedupe words: walled-catalog-tenant-writes-retired, organization_admin read-only sys_user_permission_set, organization administrator assign holders, upgrade guide replacement text", "carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed: RecordDetailView still mounts RecordPermissionAssignmentsRenderer on the sys_permission_set record page, an object objectstack main no longer registers (migration entry catalog-row-objects-retired), so that mount cannot load against a current server: dead code", "carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed: each section reads the object grant (a necessary condition), not the delegated-administration gate's adminScope, which can still refuse a holder of the grant; the same object-verdict posture as every row of the map" ], "files_changed": [ ".changeset/12082-setup-holders-grant.md", "packages/app-shell/README.md", "packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx", "packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx", "packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx", "packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx", "packages/core/README.md", "packages/plugin-form/src/affordanceGrantMap-12082.test.tsx" ], "line_budget": "n/a: no skills/** or governed ledger path touched; diff vs base f78efdd over the 8 files: 476 insertions, 93 deletions", "deviations": [ "File surface: every changed path is in the claim's surface. The app-shell README's Setup catalog section and PositionHoldersSection's docblock are counted under 'the docs that name the map's rows' and the component; it also dropped that docblock's claim that a tenant administrator assigns under a wall. affordanceGrants.ts is untouched (no row needed). The feed pin's mount() helper gained an objects parameter for the note (e) case. metadata-admin/i18n.ts untouched (no new string).", "The first locked run selected the whole metadata-admin directory (510 test files, far wider than the seam). After 16 minutes holding the shared lock with no output, I stopped my own vitest process (PID 26086, ancestry verified to my lock run 24744); TESTS_EXIT=143 is that kill and reads NOT MEASURED. Replaced by the declared narrowed 42/43-file selection; CI runs the rest.", "Two lock acquisitions hit queue-timeout (exit 99, slot kept) and were re-taken under the same slot; not a failure.", "git fetch origin main:refs/os-12082/main in the shared /home/user/objectstack checkout also advanced that checkout's shared refs/remotes/origin/main (34b740f2d..570f01d27) as an opportunistic update; no effect on any diff. My named refs (refs/os-12082/main there, refs/os-12082/branch in objectui) are deleted.", "Live HTTP measurement of the org admin's refusal: NOT MEASURED. Reason: no built objectstack stack in this container, and building and booting it is outside this card. Reach is read from objectstack main 570f01d27 source plus its own pin.", "Commit trailers are the model-free pair (AGENTS.md rule), not the harness reminder's model-bearing Co-Authored-By; the merge commit was amended before push to carry them. The PR footer uses os-dev's session-URL form.", "check:readme-exports is a partial reading (see gates); the prerequisite build covers app-shell's closure only.", "Worktree /home/user/objectui-issue-12082 removed after the PR opened (node_modules first, no --force). The local branch ref stays in the shared objectui .git, matching the pushed head 7e9eed3." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{ "issue": 12082, "status": "done", "branch": "claude/issue-12082-setup-assignment-census", "pr": "https://github.com/objectstack-ai/objectui/pull/12685", "session": "session_01Vk15bz8xHtKGUfXfCm16h4 (subagent mode: the dispatching seat's id)", "premise_still_valid": true, "summary": "Patch round on PR 12685: the one gap the seat's browser check measured. AssignedUsersSection's local copy table splits `empty` into the statement (en 'No users assigned yet.', zh '还没有分配任何用户。') and the click-Add instruction `emptyAddHint` (en 'Click \"Add user\" to assign.', zh '点击「添加用户」来分配。'), joined by a copy-local `sentenceGap` (a space in en, none in zh). The instruction renders only under `canAdd`; the statement renders either way. With `canAdd` open the rendered sentence is byte-identical to the old copy in both locales (the ablation's Received lines show it). `metadata-admin/i18n.ts`, `PositionHoldersSection` and the three out-of-round items are untouched. Base: `git merge-tree --write-tree HEAD origin/main` against origin/main 15d8ca6 exited 0 with no conflict, so main was NOT merged; the new head is cf08306, one commit on 7e9eed3.", "tests": "All at head cf08306 (the fix committed before every run). Locked: `pnpm exec vitest run --maxWorkers=2` over 5 files in repo-root form (AssignedUsersSection.contractEnvelope-5945, AssignedUsersSection, PositionHoldersSection, setupHoldersGrant-12082, and plugin-form affordanceGrantMap-12082) → 'Test Files 5 passed (5)', 'Tests 83 passed (83)', TESTS_EXIT=0, 'VERDICT command-exit 0'. Locked verbose run of setupHoldersGrant-12082 → 'Tests 18 passed (18)' (14 earlier cases + 4 new): describe 'AssignedUsersSection's empty state names Add only where Add is offered, in $locale' for 'en-US' and 'zh-CN', each with 'with the create grant closed, it says nobody is assigned and does not say to click Add' (delete-only grant: no Add button, and the empty state's text does not contain the Add label) and 'with the create grant open, it says both, and the button it names is offered' (create-only grant: the Add button is offered and the empty state's text contains its label). The two grants flip create and delete in opposite directions, so a gate on the wrong row reads red too. Ablation A4, one-time, locked, objectstack scripts/ablation-replace.mjs wrap mode: anchor '{canAdd ? c.empty + c.sentenceGap + c.emptyAddHint : c.empty}' x1 → x0, replacement '{c.empty + c.sentenceGap + c.emptyAddHint}' x0 → x1, blob 91688f271f3d → ee5efe8d414f ('ok mutation landed'). Red: 'Tests 2 failed | 16 passed (18)', exactly the two closed-grant cases: en \"expected 'No users assigned yet. Click \"Add use...' not to contain 'Add user'\" and zh \"expected '还没有分配任何用户。点击「添加用户」来分配。' not to contain '添加用户'\"; both open cases stayed green. Restore proved by the tool ('ok restored: blob == HEAD (91688f271f3d) and git diff HEAD is empty') and again by hand: git diff HEAD 0 bytes, status porcelain 0 lines, anchor count back to 1, hash-object == HEAD blob 91688f271f3d. Direction observed: red (the common case). No dist preflight: the subjects resolve through relative imports and vitest src aliases.", "gates": "Local subset, objectui, the dispatch's list (objectui has no dispatch-gates.mjs, so no --ran reconciliation exists). Closure build, locked: `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build` → 'VERDICT command-exit 0', 'Scope: 29 of 47 workspace projects'. Type-check, locked: `pnpm --filter @object-ui/app-shell type-check` → 'VERDICT command-exit 0', TC_EXIT=0, echoes 'tsc --noEmit && tsc -p tsconfig.test.json' (that program includes 'src/**/*.test.tsx'), 0 'error TS' lines. eslint on the 2 changed files: ESLINT_EXIT=0, 0 errors, 11 warnings, all on pre-existing lines outside the added hunks; the test file has none. `node scripts/check-changeset-presence.mjs` exit 0 ('5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'). `pnpm check:new-line-citations` exit 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0'). Raw control-byte grep of the 2 files: none (grep exit 1). CI at cf08306, read once at 2026-10-16T14:30Z and not waited on: 35 check-runs; 8 success (Changeset Overwrite Report, Changeset Bump Policy, Changeset Fixed Group Check, Line Citation Gate, label, Changeset Declaration, Governed Surface Queue Guard, Changeset Claim Re-read); 3 skipped (Test (coverage), Test (coverage shard matrix), dependabot); 24 in_progress (Lint, Type Check, Repo Gates, Build & E2E, Build Docs, Bundle Analysis, Test (dist pins), Test (shard 1..16/16), Live E2E (informational)). NOT MEASURED: the rest of the farm, reason: CI's.", "mcp_calls": "0", "api_writes": "1 relay stroke = 1 x POST /repos/objectstack-ai/objectstack/dispatches (fleet-write), executing 1 REST write on objectstack-ai/objectui as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectui/issues/12082/comments (this report). Plus 1 git push of the branch (7e9eed3..cf08306, fast-forward), not REST. No label, assignee or body write on the card or the PR.", "open_questions": [ { "question": "PR 12685's body does not mention the empty-state copy, and its readings stop at 7e9eed3. The seat writes the body; which edit?", "options": [ "A: under 'What changes', after the bullet that begins 'A closed row withholds the Add button', add: 'The permission set's empty state follows Add. It says No users assigned yet. under every grant; its sentence Click \"Add user\" to assign. (zh 点击「添加用户」来分配。) is said only where Add is offered. The component's own copy table splits the empty state into the two sentences, and metadata-admin/i18n.ts is unchanged. PositionHoldersSection's empty copy names no button and stays.' Then add a section 'Patch round at head cf08306': main not merged (merge-tree against 15d8ca6 clean); 5 suites, 83 tests passed; setupHoldersGrant-12082 18 passed, including the four empty-state cases (en and zh, create closed and open); app-shell type-check exit 0; eslint 0 errors; changeset-presence and line-citation gates exit 0; ablation A4 (instruction rendered unconditionally): 'Tests 2 failed | 16 passed (18)', exactly the two closed-grant cases, restore proved.", "B: leave the body as is and let the seat's review comment carry this round." ], "recommendation": "A, because reviewers read the 'What changes' list, and the list should name every behaviour the PR changes; this round's readings belong beside the earlier rounds' readings." } ], "out_of_scope_findings": [], "files_changed": [ "packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx", "packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx" ], "deviations": [ "The changeset .changeset/12082-setup-holders-grant.md is left as it is. Nothing in it is now untrue: it says a closed row withholds the Add button, the picker and each Remove, and that the holders still list. It does not name the empty-state sentence, and the dispatch allows prose changes only to correct untrue text.", "The closure build ran through the lock as a nohup launch, waited on with a foreground `tail --pid`, so that a long build stays outside the foreground cap; its VERDICT line is read from the log. Every other locked run was in the foreground.", "The merge-tree output went once to a /tmp file before I switched to the per-issue scratchpad directory. That file is deleted.", "Commit trailers are the model-free pair from AGENTS.md, not the harness reminder's model-bearing Co-Authored-By.", "Worktree /home/user/objectui-issue-12082 is removed (node_modules first, then git worktree remove without --force, after git status was empty). The local branch ref in the shared objectui .git stays at cf08306, the pushed head." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
ACCEPT → PR #12685 comment 6218195489 ·
domain:uiseat 3 ·session_01Vk15bz8xHtKGUfXfCm16h4· 2026-10-16T14:55Z. The seat landed it through the relay (pr_ready+automerge_enable); on MERGED,Fixescloses this card. Filed from its browser check: objectstack-ai/objectstack#23960 and objectui#12706.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Landed ·
domain:uiseat 3 ·session_01Vk15bz8xHtKGUfXfCm16h4· 2026-10-16T15:11Zfac2c24onmain(2026-10-16T15:10Z). Headc463195: ACCEPT6218195489, with the seat's browser check of org admin, platform admin and member reach in it.mainatfac2c24:AssignedUsersSection.tsxasksresolveAffordance('relatedNew', …)and drawsemptyAddHintonly under it;PositionHoldersSection.tsxasksresolveAffordance('relatedRowDelete', …).UNMAPPED_BACKLOGis empty. This card closedcompletedon the PR'sFixes. The seat stripspm:dispatchedin the same stroke as this note.organization_adminsets) and objectui#12706 (Setup item pages by direct URL; a refused holders read drawn as empty).Generated by Claude Code