Repository navigation
security(list sort): the Sort picker offers a field the caller may not read — choosing it blanks the list on a 403 (the Filter panel's sibling, objectui#11925) #11943
Description
Activity
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·priority:p3·domain:ui·area:access·pm:queue. Direction: the Sort picker uses the samecheckFieldread as the columns and the filter listTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T10:04Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/plugin-list/src/ListView.tsx(sortFields) ⇒domain:ui; rationale: a published library in objectui.- Why p3, and why not
security: the server's guard holds and no value is revealed. The defect is a dead end that blanks the list. The server-side disagreement is metadata read: for a platform-admin caller the object read and the sortability projection keep a field that caller's field-level permission marks unreadable, while the data route refuses it objectstack#22250. - The ruling the card asks for: an unreadable field already in the current sort stays visible only as removable, marked unavailable and never offered as a new choice. Otherwise a stored or URL sort on it would blank the list with no way out. This is a mechanism choice inside the guard objectui#11925 set, not a product fork.
- Also fix the
effectiveFieldscomment, as the card says. Pins per case, with a reverse check.
- Why p3, and why not
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsClaim: PM loop round 5
Session:session_01MgfduSkFrfM3eorB3UGfAU
Account:os-bill
Branch:claude/issue-11943-sort-field-read
Worktree:objectui-issue-11943
Domain:domain:ui
Seat:domain:ui#2
File surface (on1c51e97):packages/plugin-list/src/ListView.tsx, in two places:- the Sort picker's field list (
sortFields, about:4097–:4119) applies the sameperms.checkField(…, 'read')read andperms.isLoadedgate as the columns and objectui#11925's filter list. Per triage's ruling (6057478267), an unreadable field already in the current sort stays listed only as removable: marked unavailable, never offered as a new choice; - the
effectiveFieldscomment (about:3159–:3200) is corrected to state what is true.
- the Sort picker's field list (
- The tests beside it, and
.changeset/11943-*.md.
⛔ Not on it:
- the Filter panel's list (objectui#11925, landed), beyond reading its helper;
SortBuilderinpackages/componentsand its props, unless the "unavailable" mark needs no new prop. A new prop is a stop.- the server guard (metadata read: for a platform-admin caller the object read and the sortability projection keep a field that caller's field-level permission marks unreadable, while the data route refuses it objectstack#22250);
packages/components/src/ui/**andpackages/i18n/**.
Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report).
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectuiover these paths: no path-derived mandate; default tier)
Clause-②: no
Responsibility:objectui plugin-list ListView: the Sort picker offers a field the caller may not read, and choosing it blanks the list on the server's 403 | the columns and (after objectui#11925) the filter list already apply checkField read | every caller with field-level read restrictions, measured by the objectui#11925 dev in a browser against a 17.7.0 server
Thread-read: 6057478267
Serial constraints cleared:noneblocking.- objectui#11925 held
ListView.tsx(PR objectui#11938). It has landed. - In-flight claims and open objectui PRs, read 2026-10-08T10:56Z: none touches
ListView.tsx.
Why
Clause-②: no: an internal field list inListView. No export, prop, spec key orpackages/i18nkey. If the "unavailable" mark needs one, the dev stops and the seat re-claims.
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 11943, "status": "blocked", "branch": "claude/issue-11943-sort-field-read", "pr": null, "session": "session_01MgfduSkFrfM3eorB3UGfAU", "premise_still_valid": true, "summary": "Stopped before writing code, on the dispatch's H2 stop clause: SortBuilder cannot express 'present but not choosable' with its existing props, so the triage ruling's removable-only case needs a new SortBuilder prop (the seat re-claims with Clause-② yes). Only the PM can lift that. PREMISE holds, measured on main 1c51e97 with a temporary probe test (never committed; deleted, git status empty afterwards): through MePermissionsProvider with a /me/permissions answer marking fls_ticket.secret_note unreadable, the real Sort popover's field dropdown offers 'Secret Note'. H1 measured: the objectui#11925 read is a closure, canRead, local to the filterFields useMemo, not a component-level helper. Reusing it without forking a second predicate means hoisting it to component scope, a ListView-internal move. Before perms.isLoaded the columns filter nothing (effectiveFields guards with 'if (perms?.isLoaded && schema.objectName)') and canRead answers true, so the sort list must also filter nothing until the answer loads. H2 FALSIFIED: SortBuilderProps.fields is typed as an array of {value, label}. Every row's field Select renders one SelectItem per entry from that single shared list (key, value, label text only), and addItem seeds fields[0]?.value. On the real SortBuilder the probe passed a fields entry carrying disabled: true. The option rendered with no aria-disabled=true and no data-disabled; clicking it emitted onChange with that field; Add sort seeded the flagged field (onChange [b, a]). A kept unreadable field would therefore be offered as a new choice in every other row, and by Add sort when it is first. ALSO: no existing i18n key can carry a visible 'unavailable' mark in the sort list. list.* has only sortByIdSuffix '(by ID)', sortRelationalHint and resetSortToDefault; sortBuilder.* has the 7 row-control strings. So a text mark would also need a new packages/i18n key, the dispatch's second stop. The ListView-only alternatives fail the ruling. Dropping the field even when in use is excluded by the ruling. Listing it as today leaves it choosable. Rendering unreadable rows outside SortBuilder forks the row UI and still needs a key for the mark. Worktree /home/user/objectui-issue-11943 is kept clean at base for the re-claim; branch pushed at 1c51e97, no commits.", "tests": "No diff, so no gates are owed and none were run: NOT MEASURED for every family the dispatch lists (vitest ListView* set, column-identity ratchet, plugin-list type-check, changeset presence and no-major, check:new-line-citations, check:control-bytes, eslint), reason: stopped before implementation. HEAD: 1c51e973b (git rev-parse --short HEAD; base, no commit). The measurement runs were two runs of the temporary probe under the lock (OS_VERIFY_LOCK_SLOT=issue-11943), with pnpm exec vitest run packages/plugin-list/src/__tests__/zz-probe-11943.test.tsx from the worktree root. Run 1 had the readings as console output, which the setup suppresses: 'Test Files 1 passed (1) / Tests 2 passed (2)', 'os-verify-lock: VERDICT command-exit 0 · held the lock 14s · waited 183s (3m03s)'. Run 2 turned the readings into assertions that fail in the other direction: expect labels toContain 'Secret Note'; option aria-disabled not 'true' and no data-disabled; onChange fields [['a']] after choosing the flagged option; [['b','a']] after Add sort. Its output was 'Test Files 1 passed (1) / Tests 2 passed (2)', 'os-verify-lock: VERDICT command-exit 0 · held the lock 15s · waited 396s (6m36s)'. Probe deleted after the run, git status --porcelain empty.", "files_changed": [], "deviations": "None against the dispatch. A probe test file existed in the worktree only for the two measurement runs and was never committed. The write budget used is git push (empty branch at base, the claim's landing marker) plus this comment. No pr_create and no label-write, because there is no PR to assign.", "mcp_calls": "0 - no MCP GitHub tool used", "api_writes": "1 - this comment through scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectui/issues/11943/comments. git push is not a REST write. Reads only otherwise (single-card REST reads of the body and comments of objectui#11943 and objectui#11925).", "open_questions": [ { "question": "The triage ruling (comment 6057478267) keeps an unreadable field that is already in the current sort listed only as removable: marked unavailable and never offered as a new choice. SortBuilder cannot express that with its existing props. Which route does the seat re-claim on?", "options": [ "A1 - Clause-② yes, with the surface widened to packages/components/src/custom/sort-builder.tsx (outside ui/**). Add an optional disabled boolean on a fields entry. SortBuilder passes it to SelectItem disabled, addItem seeds the first enabled field, and Add sort is disabled when none is enabled. ListView sets disabled on a kept unreadable entry. The mark is the primitive's native disabled rendering in every row's dropdown. No i18n key. Cost: one optional prop on a published component with 4 render sites, existing callers unchanged, a components changeset. The row's own closed trigger shows the label unmarked.", "A2 - A1 plus a visible text suffix in the trigger, in the same shape as list.sortByIdSuffix. Cost: a new list.* key in packages/i18n across every locale, the dispatch's second stop, so it is also a re-claim item.", "B - Split. Land now, inside today's fence and Clause-② no, the read for every field not in the current sort, plus the effectiveFields comment. That closes the headline (an unreadable field offered as a new choice when no sort names it). An in-use unreadable field stays listed exactly as today, choosable and unmarked. The PR body opens with Part of #11943, and the removable-only half waits for an A re-claim. Cost: two PRs, and a known half-state where an in-use unreadable field is still offered in other rows. That is not a regression." ], "recommendation": "A1. It is the only route that meets the ruling's 'never offered as a new choice' half exactly. The choice is rendered in SortBuilder, so a ListView-side guard could only refuse a choice after offering it. It adds no metadata key and no i18n key. The prop is one optional flag that existing callers never see. Use A2 only if the seat reads 'marked unavailable' as requiring text in the closed row. B is the fallback if the published-prop step cannot be taken soon: it ships the uncontested half without forcing the ruling." } ], "out_of_scope_findings": [ "carrier: the Clause-② re-claim of objectui#11943 · noted, not filed. The existing in-use exception for a platform-refused field (the objectui#4243 and #6455 comment block above sortFields) has the same shape: kept in the one shared list, so it is also choosable in every other row and seedable by Add sort. The A1 flag would make that exception removable-only too. No wrong answer at a public door was measured, because the persist boundary already strips it from emits and the first row already carries it in the request." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 11943, "status": "done", "branch": "claude/issue-11943-sort-field-read", "pr": "https://github.com/objectstack-ai/objectui/pull/11962", "session": "session_01MgfduSkFrfM3eorB3UGfAU", "premise_still_valid": true, "summary": "Ruling B is implemented as draft objectui#11962. Its first line is 'Part of #11943' and 'Clause-②: no' starts a line. objectui#11925's read moved out of the filterFields memo into one module-level predicate, canReadField(perms, objectName, field): perms.checkField(objectName, field, 'read') behind the same isLoaded gate as effectiveFields. The filter list and sortFields both call it. The filter list is unchanged, and its 7 objectui#11925 pins run unedited and green. sortFields drops every unreadable field the current sort does not name. A field the current sort names stays listed exactly as before. Before isLoaded nothing is filtered, matching the columns. A dropped unreadable lookup no longer raises the relational hint. The effectiveFields comment now states which lists are built from it and which ask the read themselves, and it names the in-use exception and its follow-up. The PR body names the left-over half in one sentence: an in-use unreadable field stays listed, unmarked and choosable in other rows until a SortBuilder change. BUDGET BOT (PR comment 6059267593, Bundle Analysis success): eager closure 3307.1 KB gzip over 337 chunks against the 3312.0 KB ceiling, Status PASS; main entry chunk 71.3 KB against 350 KB. Main's own reading at d92b2a1 is NOT MEASURED. Main pushes post no budget comment, and the container proxy refused the job-log blob download for main's Bundle Analysis job 113284153789. The nearest proxy is the last budget comment on objectui#11957 (the PR that became d92b2a1): 3306.6 KB over 336 chunks, measured on that PR's head before merge, not on d92b2a1. CI at report time on 985ec3c: 42 check runs, 25 success, 3 skipped, 14 in_progress (the 8 test shards, Type Check, Lint, Spec Main Shape Gate, Doc Snippet Type Check, README Export Check, Skill Example Check), 0 failed. The worktree has been removed.", "tests": "HEAD the union ran on: 985ec3c (git rev-parse --short HEAD, the only commit; the branch was fast-forwarded to main d92b2a1 first). GATES (command, exit, verdict line): (1) os-verify-lock pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' build: VERDICT command-exit 0, 'Scope: 13 of 47 workspace projects'. (2) pnpm --filter @object-ui/plugin-list type-check (echoed 'tsc --noEmit && tsc -p tsconfig.test.json'): exit 0; tsc -p tsconfig.test.json --listFilesOnly lists the new test file. (3) pnpm exec vitest run --maxWorkers=2 packages/plugin-list/src/__tests__/ListView packages/core/src/utils/__tests__/column-identity.ratchet.test.ts, covering all 82 ListView* files including the new one and objectui#11925's, plus the ratchet: 'Test Files 83 passed (83)', 'Tests 789 passed (789)', lock VERDICT command-exit 0. (4) node scripts/check-changeset-presence.mjs exit 0 '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/11943-sort-field-read.md.'; node scripts/check-changeset-no-major.mjs exit 0 'No changeset declares a major bump.'; check:new-line-citations exit 0 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only'; check:control-bytes exit 0 OK. Added from the diff, all exit 0: check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:phantom-deps, check:unreferenced-sources, check:i18n-keys, check:shell-escape-residue. (5) LINT, narrowed and measured: eslint --format json over the 2 touched TS files, the config's '**/*.{ts,tsx}' population, gave 2 files and 0 errors. ListView.tsx has 188 warnings, equal to its base blob linted over stdin (188); the new test has 0. eslint.config.js enables no type-aware linting and no eslint-rules rule reads the filesystem, so untouched files cannot move; repo-wide lint is CI's. REVERSE CHECK on the committed fix, under one lock hold (VERDICT command-exit 0), each mutation through scripts/ablation-replace.mjs wrap mode with anchor 1 to 0, blob 0edf2348fdde changed, restore blob equal to HEAD 0edf2348fdde and git diff HEAD empty, run over the new file plus objectui#11925's file. M1 deleted the sort read line (fix reverted): predicted pins 1, 3, 5; observed 'Tests 3 failed / 9 passed (12)', exactly those. M2 dropped the isLoaded leg of canReadField: predicted pin 3; observed 1 failed / 11 passed, pin 3. M3 dropped the in-use exception: predicted pin 4; observed 1 failed / 11 passed, pin 4. The 7 objectui#11925 pins stayed green under all three. After the run: git diff HEAD 0 lines, status 0. No dist was involved, because the test imports ../ListView from source.", "files_changed": [ "packages/plugin-list/src/ListView.tsx", "packages/plugin-list/src/__tests__/ListView.sortFieldRead-11943.test.tsx", ".changeset/11943-sort-field-read.md" ], "deviations": "(1) ROUTE: the seat asked for canReadField at component scope inside ListView; it is at module scope in ListView.tsx instead. A component-scope version is either a useCallback identity in two useMemo dependency lists, which AGENTS.md #10 forbids, or a per-render closure that react-hooks/exhaustive-deps flags in both memos. As a plain function of perms and schema.objectName, which both memos already list, it needs neither. Stated in the PR body. (2) ASSIGNEE NOT WRITTEN: 'node /home/user/objectstack/scripts/pm/label-write.mjs --issue 11962 --repo objectstack-ai/objectui --assign os-bill' was refused by the Claude Code auto-mode classifier before running, reason '[External System Writes]'. No request was sent and no other route was tried. The seat writes the PR assignee os-bill. (3) Main's own budget reading is NOT MEASURED; the reason is in the summary. (4) The branch was fast-forwarded from 1c51e97 to main d92b2a1 before the change; none of the merged files touch plugin-list.", "mcp_calls": "0 - no MCP GitHub tool used", "api_writes": "3 over this claim. (a) The round-1 blocked report: comment 6058711345 through scripts/pm/post-stamped.mjs, POST /repos/objectstack-ai/objectui/issues/11943/comments. (b) pr_create through scripts/pm/fleet-write/dispatch.mjs --session session_01MgfduSkFrfM3eorB3UGfAU, request fw-20261008T115023Z-f7eaa9, run 37772723615 success, relayed as POST /repos/objectstack-ai/objectui/pulls (draft): objectui#11962, 5972 bytes sent and 5972 stored, identical. (c) This comment through scripts/pm/post-stamped.mjs, POST /repos/objectstack-ai/objectui/issues/11943/comments. The assignee label-write was refused before sending, so it is not a write. git push (twice: the empty marker and the fix commit) is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "carrier: the seat's return of objectui#11943's removable-only half to triage · noted, not filed. From source reading only: the hide-fields popover's list (allFields in ListView.tsx) is built from the declared columns with no read check. A restricted user can see an unreadable column's label there, and toggling it changes nothing because the column is already gone. The old effectiveFields comment claimed the opposite; the new one makes no claim about that popover. Recorded in the PR's Acceptance notes. Dedupe words: hide fields popover field level read · allFields checkField · FLS hide-fields list.", "carrier: the same follow-up · noted, not filed. The platform-refused in-use exception above sortFields (objectui#4243 and #6455) has the same shape as the half left here: it is also choosable in other rows and seedable by Add sort. The A1 SortBuilder flag from the round-1 analysis would cover both." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsReview: ACCEPT (partial, route B) — PR objectui#11962 ·
domain:uiseat 2 · 2026-10-08T11:57ZImplemented-by:
os-devsubagent ofsession_01MgfduSkFrfM3eorB3UGfAU, branchclaude/issue-11943-sort-field-read, head985ec3c. Reports6058711345(round 1, stopped on the prop) and6059313740.
Reviewed-by:domain:uiseat 2 (session_01MgfduSkFrfM3eorB3UGfAU), against GitHub and the branch diff, not against the report's prose.Route, ruled by the seat:
- Round 1 measured that
SortBuildercannot express "present but not choosable": adisabledentry is ignored and still seeded by Add sort. - No existing translation key can carry a text mark.
- So triage's removable-only half (
6057478267) needs a published prop:Clause-②: yes, which is not this seat's lane. - This PR ships the uncontested half inside the fence, as
Part of #11943. The remainder returns to triage after it lands.
Checklist:
- PR shape: draft, first line
Part of #11943, with no closing keyword next to 11943.Clause-②: nois at the start of a line. - Assignee: none. The dev's
label-write --assignwas refused by its permission layer before any request was sent, and the seat does not perform a refused write on a dev's behalf. - Scope: 3 files inside claim
6058334318:ListView.tsx, a new pin and the changeset.SortBuilderandpackages/i18nare untouched. - The fix: objectui#11925's read moved into one module-level predicate,
canReadField(perms, objectName, field).- It is the same
checkField(…, 'read')behind the sameisLoadedgate as the columns. - The filter list and
sortFieldsboth call it. - Module scope rather than component scope avoids a
useCallbackidentity in two memo dependency lists (AGENTS.md [WIP] Enhance every detail of the designer #10). Accepted.
- It is the same
- Behaviour:
- An unreadable field the current sort does not name is not offered.
- A field the sort names stays listed as before. That is the known half-state, pinned and named in the comment and the PR body.
- Nothing is filtered before
isLoaded. - A dropped unreadable lookup no longer raises the relational hint.
- The filter list is unchanged: its 7 objectui#11925 pins pass unedited.
- The
effectiveFieldscomment now states which lists ask the read themselves, and it names the in-use exception and its follow-up. - Changeset:
@object-ui/plugin-list: patch. Every sentence was checked against the diff, including the follow-up sentence. - Evidence:
- All 82
ListView*files plus the ratchet pass: 83 files, 789 tests. - The type-check passes.
- Three mutations each turned exactly their predicted pins red: the read removed, the
isLoadedleg dropped, and the in-use exception dropped. objectui#11925's pins stayed green, and each restore was proven.
- All 82
- First-load bytes: the budget bot reads 3307.1 KB against 3312.0 KB, a PASS.
- CI:
985ec3cis running. The PR lands through the merge queue when every check is green. Then the card goes back to triage for the removable-only half, asClause-②: yes, with round 1's A1 analysis.
Out-of-scope note, carried to that release: the hide-fields popover's list (
allFields) applies no read check, so it shows an unreadable column's label. This was read from source, not measured.
Generated by Claude Code
- Round 1 measured that
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsPartial landing and release for triage: the remaining half needs a
Clause-②: yesroute ·domain:uiseat 2 · 2026-10-08T12:26ZRelease:
session_01MgfduSkFrfM3eorB3UGfAU(domain:uiseat 2, accountos-bill) closes claim6058334318.Delivered. PR objectui#11962 (
Part of #11943) merged ase44f9bc, checked onorigin/mainby content:- The Sort picker drops every field the caller may not read that the current sort does not name.
- It uses one module-level predicate,
canReadField, which is objectui#11925'scheckField(…, 'read')behind the sameisLoadedgate. The filter list now calls it too, with its behaviour unchanged. - The
effectiveFieldscomment states what holds. - The seat's review is
6059337381.
Remaining: triage's removable-only half (
6057478267): an unreadable field already in the sort is listed only as removable, marked unavailable and never offered as a new choice. Today that field stays listed exactly as before, so it is still choosable in other rows and seedable by Add sort. This is a known half-state, pinned and named in the code comment. It is not a regression.Why it leaves this lane: the dev's round-1 measurement (
6058711345) on1c51e97:SortBuilderProps.fieldsis{ value, label }[], rendered as one shared list.- A
disabledflag on an entry is ignored: noaria-disabled, it is still choosable, and it is still seeded by Add sort. - No existing translation key can carry a visible "unavailable" mark.
So the half needs a new optional prop on the published
SortBuilder, and possibly a newpackages/i18nkey. That isClause-②: yes: spec-lane work behind a contract review.The dev's options, for the next owner:
-
A1, recommended:
- an optional
disabledon afieldsentry; SortBuilderpasses it toSelectItem;- Add sort seeds the first enabled field, and is disabled when none is enabled;
ListViewsets the flag on a kept unreadable entry.
No i18n key. It also covers the platform-refused in-use exception above
sortFields(objectui#4243 / ListView's sort picker persists a platform-refused sort — the in-use exception re-emits it, andfilterPlatformSortableSorthas only one consumer #6455), which has the same shape. - an optional
-
A2: A1 plus a visible text suffix, which needs a new
list.*key in every locale.
Out-of-scope note, read from source, not measured: the hide-fields popover's list (
allFieldsinListView.tsx) applies no read check, so a restricted user sees an unreadable column's label there.Labels:
pm:dispatched→pm:queuepluspm:retriage. The assignee is cleared.
Generated by Claude Code
3 remaining items
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsRetriage answered: the removable-only half goes to
domain:specasClause-②: yes, direction A1.pm:retriageremovedTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T13:07Z. ⛔ Not a claim, ⛔ not a dispatch.Thread-read: 6059829577
- Route:
domain:ui→domain:spec. Your reading is right:execution-duties.md:101says a card that widens a published surface is spec-lane work, and an optionaldisabledonSortBuilder's publishedfieldsentries widens it. See my correction on objectui#11863 and objectui#11860, posted this round. - Direction: A1.
- An optional
disabledon afieldsentry. SortBuilderpasses it toSelectItem.- Add sort seeds the first enabled field, and is disabled when none is.
ListViewflags a kept unreadable entry.- No new i18n key, so A2 is not taken. A1 also covers the platform-refused in-use case (objectui#4243 / ListView's sort picker persists a platform-refused sort — the in-use exception re-emits it, and
filterPlatformSortableSorthas only one consumer #6455).
- An optional
- The hide-fields popover note (
allFieldsapplies no read check): read from source, not measured. Measure it at this claim; if it reproduces, it is a card.
- Route:
- addeddomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneand removeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 (this card's remaining half: triage's removable-only case, direction A1 per retriage
6060561797)
Session:session_01DBZ9bntPZ7VKyQNtJeNsgw
Account:huangyiirene(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-11943-sort-disabled-entry
Worktree:objectui-issue-11943-a1
Domain:domain:spec
Seat:domain:spec#1
File surface (line numbers on objectuimain0c9fb8ea):packages/components/src/custom/sort-builder.tsx:SortBuilderProps.fieldsentries gain an optionaldisabled.SortBuilderpasses it toSelectItem. Add sort seeds the first enabled field and is disabled when none is enabled.packages/plugin-list/src/ListView.tsx, thesortFieldsmemo only (about:4143–:4167), plus the comment above it (about:4121–:4131). A kept unreadable entry, and the kept platform-refused one (objectui#4243 / ListView's sort picker persists a platform-refused sort — the in-use exception re-emits it, andfilterPlatformSortableSorthas only one consumer #6455), carry the flag. The comment is corrected to state what holds.- The tests beside these, and one
.changeset/11943-*.mdnaming@object-ui/componentsand@object-ui/plugin-list, at the bump objectui'sAGENTS.mdsets for an additive optional prop.
⛔ Not on it:
packages/i18n/**(A2 was not taken: no new key);packages/components/src/ui/**;ListView.tsxoutside thesortFieldsregion: the grouping editors andListViewPropsbelong to objectui#11860, which is queued in this lane;- the hide-fields popover's
allFields: measured here, reported, not fixed; - the other
SortBuildercallers (config-field-renderer.tsx,SortUI.tsx,ObjectListViewInspector.tsx): unchanged, because the flag is optional.
Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report).
Container & model:S,mode:subagent,model: default(dispatch-gates --tier --repo objectstack-ai/objectuiover these paths: "no path-derived mandate: the surface hits none of the 3 declared glob(s)"; clause ② forbids a build belowdefault). The contract review is owed atCONTRACT_REVIEW_TIER, by an isolated subagent, because this seat is not serving at that tier.
Clause-②: yes
Ruling-ref:6060561797(triage, direction A1), on top of6057478267(the removable-only ruling)
Responsibility:@object-ui/componentsSortBuildercannot mark an entry as present but not choosable, soListViewkeeps an unreadable in-use field choosable in every row and seedable by Add sort | none: the server's guard answers 403 and the list blanks | a restricted user with a stored or URL sort on a field they may not read; measured by6058711345
Thread-read: 6060561797
Serial constraints cleared: read at 2026-10-08T13:29Z on objectuimaind7e9e9ab(thesortFieldslines are unchanged from0c9fb8ea). None of the 4 open PRs touchessort-builder.tsxorListView.tsx. This lane's in-flight work: objectui#11070 round 13 (packages/typesform faces, catalog, guide), not on this surface. PR #11969 (objectui#6152 round 8) merged asd7e9e9ab. objectui#11860, queued in this lane, sharesListView.tsx. Fold-or-serial, answered: ⛔ not folded, because gate ① fails (a sort-entry flag and a grouping-change callback are different defects with different fixes). The regions are disjoint (sortFieldsversusListViewPropsand the grouping editors), so the two may run in parallel under parallel discipline ①–④. objectui#11860 is ordered after this card by the pick order (bugbeforeenhancementat p3).Clause-② reads
yesbecause an optionaldisabledonSortBuilderProps.fieldsentries widens a published prop type of@object-ui/components.objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 11943, "status": "done", "branch": "claude/issue-11943-sort-disabled-entry", "pr": "https://github.com/objectstack-ai/objectui/pull/11981", "session": "session_01DBZ9bntPZ7VKyQNtJeNsgw (subagent; the seat's id, as the claim names it)", "premise_still_valid": true, "summary": "Direction A1, the card's remaining half. SortBuilder's fields entries take an optional disabled: passed to SelectItem, so every row's dropdown draws the entry as an unavailable option that click and keyboard cannot choose; a row whose field it already is still shows its label and can be changed or removed; Add sort seeds the first entry that is not disabled and is disabled when none is. ListView's sortFields flags exactly the entries the in-use exception alone keeps (unreadable, relational with the by-ID suffix, platform-refused or type-read-refused), nothing else; labels and membership are unchanged. The comments above sortFields and on effectiveFields now state what holds (canReadField's docblock promised nothing and is unchanged). PR objectui#11981 opens with this card's closing line (objectui#11962 already landed the read check), then the Clause-② yes line. H1-H4 all measured true; the hide-fields popover note reproduces (see out_of_scope_findings). Newest Claim comment 6060957822 named this branch before the first edit; assignee untouched; worktree removed after the PR opened.", "tests": "Union on HEAD 2b80540b7 (git rev-parse --short HEAD), through the verify lock: pnpm exec vitest run --maxWorkers=2 packages/components/src/ packages/plugin-list/ packages/plugin-view/src/ packages/app-shell/src/views/studio-design/ObjectListViewInspector.layersUnsaved-11799.test.tsx packages/app-shell/src/views/studio-design/ObjectListViewInspector.target.test.tsx scripts/__tests__/ → 'Test Files 738 passed | 3 skipped (741)', 'Tests 11143 passed | 26 skipped (11169)', vitest-exit 0; then pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' build ('Scope: 13 of 47 workspace projects', build-exit 0); pnpm --filter @object-ui/components type-check and pnpm --filter @object-ui/plugin-list type-check (both echo 'tsc --noEmit && tsc -p tsconfig.test.json', tc-exit 0; --listFilesOnly lists sort-builder-disabled-entry-11943.test.tsx in components' test program and both 11943 files in plugin-list's). Lock: 'VERDICT command-exit 0 · held the lock 1669s · waited 17s' (shared-box seconds). New pins: packages/components/src/__tests__/sort-builder-disabled-entry-11943.test.tsx (7 tests: mark vs unflagged control; click and pointer-up refused vs control; Enter and closed-trigger typeahead refused vs control; flagged row shows label, changes, removes; blank-row control; Add sort seed vs control; Add sort gate) and packages/plugin-list/src/__tests__/ListView.sortRemovableOnly-11943.test.tsx (unreadable, platform-refused, relational cases, each declared first on the object; two controls). H3: the five objectui#11962 pins ran unedited on committed fix a64e78e: 'Test Files 1 failed | 3 passed (4)', 'Tests 4 failed | 18 passed (22)', the only failing file being the temporary probe; pin file 5/5; afterwards only pin 4's title and comment changed. Reverse check on committed 2b80540b7 via objectstack scripts/ablation-replace.mjs (each leg: anchor x1 to x0, blob changed; restore blob == HEAD, git diff HEAD empty; script-level trap restoring both files from HEAD), running the 3 files (17 tests; baseline 'Tests 17 passed (17)'): M1 drop disabled pass-through → 6 failed/11 passed (3 SortBuilder + 3 ListView, as predicted); M2 seed fields[0] → 4 failed/13 (seed case + 3 ListView); M3 Add sort gate back to fields.length === 0 → 1 failed/16; M4 drop the ListView flag → 3 failed/14 (3 ListView cases); M5 drop the readable leg of the unflagged branch → 1 failed/16 (unreadable case only). objectui#11962 pins green in every leg. No dist ablation: vitest aliases @object-ui/components to src, and plugin-list tests import ListView from src. Cross-package type reverse check after the closure build: packages/components/dist/custom/sort-builder.d.ts declares 'disabled?: boolean'; a trap-deleted probe in plugin-list with disabled: 'no' → 'TS2322: Type string is not assignable to type boolean | undefined' (tsc exit 2; a stale d.ts would answer an excess-property error), disabled: true → tsc exit 0; probe gone after. Lint narrowed and measured: eslint --no-inline-config --format json over the 5 touched TS files (population: eslint.config.js files '**/*.{ts,tsx}') → 5 files, 0 errors; warnings equal base blobs over stdin (sort-builder.tsx 2, ListView.tsx 199, sortFieldRead pin 0), new tests 0; invariance: tseslint.configs.recommended with no parser project (no type-aware linting) and no file under eslint-rules/ reads the file system (recursive grep zero hits; control 'context.report' hits 11 files), so untouched files cannot move.", "gates": "On HEAD 2b80540b7: check-changeset-presence exit 0 ('5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'); check-changeset-no-major exit 0; check:changeset-claims exit 0; check:pending-changeset-literals exit 0; check:new-line-citations 'VERDICT new-cross-file-line-citations: 0 new citation(s)'; check:control-bytes OK; check:doc-types exit 0; check-governed-queue-guard --test over the 6 paths: NOT GOVERNED. NOT MEASURED: check:skill-examples (exit 2 PRECONDITION NOT MET, needs the app-shell closure built) and check:readme-exports (exit 1, '579 self-import(s) could not be judged', type entries not on disk), reason: both need a near-whole-tree build; narrowing: zero hits for SortBuilder or sort-builder under skills/, content/docs/ and every README.md (control FilterBuilder / GroupingEditor hits), and no export name moves. Bundle: NOT MEASURED, the PR's Bundle Analysis comment was not posted at report time (CI in_progress: Lint, Type Check, Test shards 1-8, Build & E2E running; Control Byte Scan, Line Citation Gate, Changeset Bump Policy, Doc Component Type Check, Changeset Fixed Group Check success).", "line_budget": "n/a: no skills/** or governed path touched", "files_changed": [ ".changeset/11943-sort-in-use-removable-only.md", "packages/components/src/custom/sort-builder.tsx", "packages/components/src/__tests__/sort-builder-disabled-entry-11943.test.tsx", "packages/plugin-list/src/ListView.tsx", "packages/plugin-list/src/__tests__/ListView.sortRemovableOnly-11943.test.tsx", "packages/plugin-list/src/__tests__/ListView.sortFieldRead-11943.test.tsx" ], "deviations": "1) H3 says the objectui#11962 pins run unedited: they did (green on a64e78e), and afterwards pin 4's title and comment were edited because they described the half-state this change ends; its assertions are byte-unchanged. 2) H2: the relational in-use case is flagged as well; reasoning in the PR body (objectui#4243 removed relational fields as new choices; the in-use entry exists only to keep its row). 3) Conflict with the dispatch, reported not resolved: it asks to quote the Bundle Analysis verdict, while os-dev.md forbids waiting on CI before reporting; it was not posted at report time, recorded NOT MEASURED. 4) check:skill-examples and check:readme-exports NOT MEASURED (precondition, see gates). 5) The SortBuilderProps edit also drops one trailing space on the fields line. 6) The pr_create relay needs an actions file, so the PR body sat in this issue's scratchpad subdirectory as 11943-pr-body.md, read back before sending and deleted after. 7) Commits end with the model-free pair (Claude-Session + Co-authored-by: Claude) per objectui AGENTS.md, not the harness's model-named trailer. 8) Two temporary probes (allFields measurement, d.ts type probe) were never committed and are gone.", "mcp_calls": "0 - no MCP GitHub tool used", "api_writes": "3 REST writes, each through the fleet-write relay as objectstack-fleet[bot] (each relay hop is one POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create POST /repos/objectstack-ai/objectui/pulls (draft, read-back 9328 bytes sent = stored, identical); (2) label-write.mjs --assign huangyiirene: POST /repos/objectstack-ai/objectui/issues/11981/assignees (read-back matches; labels package: components, plugin, tests are labeler.yml's); (3) this comment via post-stamped.mjs: POST /repos/objectstack-ai/objectui/issues/11943/comments. git push (empty branch, then 2 commits) is not REST. Reads: single-card REST reads of objectui#11943 and its comments, PR objectui#11962 and its files, PR objectui#11981 and its check-runs.", "open_questions": [], "out_of_scope_findings": [ "class: a · reach: the list toolbar (UI), measured through the real ListView with objectui#11962's MePermissionsProvider set-up in a temporary happy-dom probe (deleted): with secret_note unreadable, the Sort picker withholds it ([Title, Status, Priority], so the answer is loaded and denying) while the hide-fields popover lists [Title, Status, Secret Note]; the same allFields list feeds the Group editor's field options (Group is shown unless userActions.group is false) and the Row color field select, and both offer Secret Note too; with full read the lists are the same. Hide fields shows only with userActions.hideFields true and Row color only with userActions.rowColor true. Not measured: what a server answers when a grouping on the unreadable field is chosen (from source the grid then sends a group header query naming it). Source: allFields in ListView.tsx maps schema.columns with no canReadField. Neighbour: objectui#11860, queued in this lane, owns the grouping editors region of ListView.tsx. · dedupe words: hide fields popover unreadable column · allFields checkField · group by field level security · row color field FLS", "carrier: 承接者:无 · noted, not filed (read from source, not measured): SortBuilder's per-row remove control is an icon-only Button with no aria-label, while its sortBuilder.removeSort default string is declared and never read. Not in the PR's Acceptance notes; the seat may append it there." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsACCEPT: PR #11981 at
2b80540b, report6062759616. From thedomain:spec @ objectuiseat (objectui#10217), sessionsession_01DBZ9bntPZ7VKyQNtJeNsgw, 2026-10-08T15:39Z. Ready and auto-merge follow in this act.Contract face. The at-tier record
6063497259on PR #11981 (Served-tier: CONTRACT_REVIEW_TIER,Local-runs: none, isolated subagent) reads PASS on this head, and the seat adopts it.Clause-②: yesagrees across the claim and the PR body. The change widensSortBuilderProps.fieldsentries by one optionaldisabled, and existing callers keep their shape.- The changeset declares
@object-ui/componentsminor and@object-ui/plugin-listpatch, not breaking.
Checked against the PR, not the report:
- Draft, base
main, first lineFixes #11943. That is right: with objectui#11962 landed, this completes every Done-when item. - Not governed: 0 of 6 paths, and the change is 504 lines.
- On
2b80540b: 43 checks, 40 success and 3 skipped by design.Skill Example CheckandREADME Export Checkare success, which answers the two gates the dev could not run locally. - Bundle:
Bundle Analysisreads 3311.1 KB against the 3312.0 KB ceiling, on a merge ref with989b1908.mainhas since taken objectui#11976, whose own PR read 3311.1 KB. This diff adds 16 lines tosort-builder.tsxand changesListView.tsxby 20 / −10, so the combination stays under the ceiling. - File surface: inside the claim. The
effectiveFieldscomment edit (about:3188) is outside the namedsortFieldsregion, but the claim's exclusion reason (the grouping editors andListViewProps) does not reach it, and the card's Done-when asks for a true comment. Accepted. - Direction A1 met: the flag sits exactly on the entries the in-use exception alone keeps. Those are unreadable, relational with the by-ID suffix, or platform- or type-refused. Labels and membership are unchanged. Add sort seeds the first enabled entry and is disabled when none is.
- The relational case is flagged too. The record judged it right: objectui#4243 already removed relational fields as new choices.
- The objectui#11962 pins ran unedited and green. Afterwards one pin's title and comment were restated, with its assertions byte-for-byte unchanged. The reverse check M1–M5 matched its predictions.
The hide-fields note, as triage's retriage asked: it reproduced, through the real
ListView. The hide-fields popover, the Group editor's options and the Row color field list all offer an unreadable field. Filed as objectui#11984, for triage to grade.Out of scope, one line each:
- Acceptance notes:
SortBuilder's per-row remove control is an icon-only button with noaria-label, while itssortBuilder.removeSortdefault string is declared and never read. This was read from source, not measured. Carrier: the next PR that touchessort-builder.tsx.
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsLanded: PR #11981 merged through the merge queue as
4f4fc03f.Fixes #11943closed this cardcompleted, andpm:dispatchedcomes off in this act. From thedomain:spec @ objectuiseat (objectui#10217), sessionsession_01DBZ9bntPZ7VKyQNtJeNsgw, 2026-10-08T15:58Z.Verification
- Merge content matches the PR: the landed commit's
git patch-id --stableequals the PR's net diff against its merge based7e9e9ab(5acd976d3841on both sides). - On
main, by content:packages/components/src/custom/sort-builder.tsxdeclaresdisabled?: booleanon afieldsentry, and Add sort seedsfields.find((f) => !f.disabled). - Reviewed head is the landed head: the at-tier record
6063497259PASSed2b80540b, and nothing was pushed after it. CI there: 40 success and 3 skipped by design.
The card is done at both halves. objectui#11962 (
e44f9bc) put the read check on the Sort picker. This landing makes an unreadable, relational or platform-refused field the current sort names removable only. Such a field is drawn unavailable in every row's dropdown and is never seeded by Add sort.Carried: the hide-fields, Group and Row color lists offer an unreadable field. That is objectui#11984, with triage.
Generated by Claude Code
- Merge content matches the PR: the landed commit's
Filing gate ①: a measured defect, class (a), in the
securityfamily, though not a value leak. It is an out-of-scope finding from the objectui#11925 dev. The reach was measured in a browser and over REST against a booted, published 17.7.0 server. Kept abstract, per the triage posture on objectui#11925: classes, positions, files and functions only.Who acts on it: objectui triage, grade and route. ⛔ Not a claim. Filed by the
domain:uiexecution seat 1 (session_01DrKzdPdyLLBW3qpZ4vtk7z).What happens
Where it comes from (read in source)
packages/plugin-list/src/ListView.tsx:sortFieldsis built fromcandidateFieldswith noperms.checkField(…, 'read'). The columns (effectiveFields) and, after objectui#11938, the filter field list both apply it.effectiveFieldscomment says unreadable fields "also disappear from the … filter/sort builders". After objectui#11938 that holds for the filter builder only.Done when
checkFieldread andperms.isLoadedgate as the columns and the filter list. With full read, it lists the same fields as today (the control).effectiveFieldsso it states what is true.Duplicate check
Dedupe words: sort picker field level security · sortFields unreadable field · FLS sort builder · candidateFields checkField sort
Generated by Claude Code