Repository navigation
showcase 的 cascading-select 用 'admin' in current_user.positions 收敛选项,而 admin 从来不在服务端的 positions 轴上(membership admin 被映射成 org_admin) #15943
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdocumentationImprovements or additions to documentationImprovements or additions to documentation
on Sep 6, 2026 分诊:
domain:devx/bug+documentation/pm:queue/priority:p3落点核实(
origin/main,本轮实读,逐条对卡):卡片主张 实读 结论 cascading-select.object.ts:85用P`'admin' in current_user.positions`:85{ label: 'Restricted (admin only)', value: 'restricted', visibleWhen: P'admin' in current_user.positions},✅ 逐字成立 档注 :23-27断言服务端同规则拒绝非 admin:24`'admin' in current_user.positions`. The same rule-validator rejects a:25non-admin who submits it anyway; it fails open only when current_user is unbound…✅ 成立 membership 角色经 mapMembershipRole规范化后才进positionspackages/core/src/security/resolve-authz-context.ts:820const r = mapMembershipRole(raw);→:821if (!grants.positions.includes(r)) grants.positions.push(r);✅ 成立(卡片写 :821,规范化调用本身在:820,push 在:821——一行之差,不影响结论)内建名里没有 admincontent/docs/permissions/positions.mdx:87``org_adminpositions.mdx:87那一行是本卡最锋利的证据:它把sys_member.role = admin与位面名org_admin写在同一行,正是 showcase 踩空的那一步映射。范围内活控制:在
examples/全目录搜current_user.positions,命中 3 条——cascading-select.object.ts:24、:85(本卡两处),以及examples/app-showcase/CHANGELOG.md:3813(历史条目)。控制活着(CHANGELOG 那条是真阳性的非授权命中),因此**「showcase 里只有这一个字段用了 positions 谓词」是读数,不是搜索失效**——这不是系统性写法,是孤例,一处修完即净。⚠️ 卡片没提、但改变紧急度的一条读数: objectuie2e/live/cascading-options.spec.ts在origin/main上搜restricted|admin|tier,只命中:10一行,且是档注散文(per-option visibleWhen + field-level dependsOn, plus a role-gated tier.)——该 e2e 并未断言restricted选项对已 seed 的 admin 可见。控制:同一次搜索命中了tier,证明模式有效,所以restricted的 0 命中是读数。⇒ #15136 落地后不会有 e2e 变红来提醒任何人。这既压掉了「会自动暴露」的乐观预期,也压掉了升级理由:没有 CI 会因此断,损害停留在 showcase 自身示范错误 ⇒ p3。
定级理由
domain:devx:修复点全部落在examples/app-showcase/(谓词字符串 + 档注),不触碰packages/core或packages/spec——mapMembershipRole和内建名表都是对的,错的只有示例。examples/ 属开发者面向的示范语料,与content/docs/**同一条治理线。先例参照 showcase: the rollup filter-editor half needs a writable-package summary field — a showcase design call, not a seed addition #9788(showcase 设计取舍 →domain:devx);⛔ 不走 finding(showcase): seven showcase objects are exposed in navigation with no permission set granting read, andshowcase_field_zoois a detail object with no CRUD grant — the showcase build warns on every run #14453 的domain:services,那张卡的修复落在被服务端消费的权限集元数据上,本卡不落那里。bug:谓词在服务端恒为假,档注:23-27断言了一个不成立的服务端行为——main 上有东西是假的。修复只是把示例改回内建名,不拓宽任何接受集 ⇒ Bug/tidy 侧,无人工地板。- 同挂
documentation:一半修复面是档注。 - p3:仅 showcase 示范,无发运运行时受影响,无 CI 覆盖,孤例一处。
处置
卡片给的二选一里,方案 1(谓词改成
'org_admin')是唯一该走的,方案 2(在 seed 里造一条名为admin的sys_position)应当明确否决并写进卡片——它会让旗舰示例示范一个与内建名org_admin只差前缀的自定义位面名,正是最容易被复制成生产 bug 的形状;文档positions.mdx:90与permission-metadata.mdx:203两处都在示范'org_admin',示例应与之一致。⛔ 这不是 decision-box:一支已被文档先例答掉,另一支有明确反对理由,无需人工裁决。同时改
:24的档注,使其不再断言未成立的服务端行为。与 #15136 的关系(卡片已说明,本席核实其方向描述与代码一致):本卡不依赖 #15136,也不会因 #15136 落地而自愈——落地后两侧一致判假,选项对所有人隐藏,档注仍是假的。两卡可并行。
⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box(本会话为
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。
Generated by Claude Code
Serial constraint recorded (PM seat
domain:devx @ objectstack, sessionsession_01Vbw3RPgdtqesx4azk9SbW8, 2026-09-06T07:0xZ) — not dispatched yet; waiting on another seat's PR. This card's one file,examples/app-showcase/src/data/objects/cascading-select.object.ts, is in the changed-file list of open draft PR #16069 (claude/issue-14453-showcase-permission-gaps,os-warren), read fromGET /pulls/16069/filesat 07:0xZ. Two live PRs never share a hot file, so this card is dispatched only once #16069 is MERGED or closed (the dispatch then rebases its reading of:24and:85on the landed tree). Premise still holds onorigin/main@cbca47d0::85visibleWhen: P'admin' in current_user.positions``;:24-25still assert the server-side rejection; `content/docs/permissions/positions.mdx:87` still maps `sys_member.role = admin` → `org_admin`. Triage's disposition (option 1 only, option 2 refused) is unchanged. `pm:queue` stays; this comment is the waiting-state record, not a claim.
Generated by Claude Code
os-try-charles commented
on Sep 17, 2026 CollaboratorMore actionsPM loop round 23 ——
domain:devx执行席认领本卡(2026-09-17T12:14Z)。Claim: PM loop round 23 Session: `session_017ef78bLdybu3AffehKkhfk` Branch: `claude/issue-15943-showcase-org-admin-position` Worktree: `objectstack-issue-15943` Domain: `domain:devx` File surface: `examples/app-showcase/` (stop on breach; explain in the report) Container & model: `S 级机械卡`, `mode:subagent`, `model: opus`(`dispatch-gates --tier examples/app-showcase/src/data/objects/cascading-select.object.ts` 当次输出:「no path-derived mandate … 档位归 PM 判断」) Clause-②: no Thread-read: 5557608035 Serial constraints cleared: 上一位席位记的串行约束 **PR #16069 已于 2026-09-06T14:20Z 合并**(本轮读 GitHub 确认),该约束解除;另读 16 个 open PR 的 files(共 297 个文件名 ⇒ 读法有效),⛔ 无一碰 `examples/`;`os-verify-lock --status` 读出 lock free前提本轮重取,⛔ 行号按内容重定位
examples/app-showcase/src/data/objects/cascading-select.object.ts :24 档注:`'admin' in current_user.positions`. The same rule-validator rejects a … :94 { label: 'Restricted (admin only)', value: 'restricted', visibleWhen: P`'admin' in current_user.positions` }, ⚠️ 卡面写的是 :85 —— 那是立卡时的行号,今天谓词在 :94。⇒ 承接者按**内容**定位,⛔ 不按卡面行号。 CONTROL 同一读法:`grep -rn 'current_user.positions' examples/` 命中 3 条 ⇒ 读法有效 文档正面示范(同一棵树):content/docs/permissions/positions.mdx:90 Test membership in RLS/CEL with `'org_admin' in current_user.positions`.派发面 = 分诊裁定的选项 1,⛔ 选项 2 已被分诊否决
- ✅ 谓词改为
'org_admin' in current_user.positions(与内建名一致)。 - ✅ 同笔修正
:23-27的档注 —— 它现在断言了一个服务端并不成立的行为。 - ⛔ 不在 showcase 的 seed 里新建一条名为
admin的sys_position(分诊已否:那会示范一个与内建名极易混淆的岗位名)。 - ⛔ 不碰
packages/**、⛔ 不碰content/docs/**、⛔ 不扩到其它示例 app。
验收(两个方向都要)
- 改后一个
sys_member.role = 'admin'的用户在服务端positions里是org_admin⇒ 谓词为真、选项可见,且 rule-validator 与之一致 —— 这正是档注一直声称、而今天不成立的那句话。 - 发火对照:一个非 admin 用户仍看不到该选项(⛔ 不能靠把谓词改松来让测试变绿)。
- 若你测得「改后两侧仍不一致」,⛔ 停下来回报,不要靠改 seed 或改 validator 去凑。
Generated by Claude Code
- ✅ 谓词改为
os-try-charles commented
on Sep 17, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 15943, "status": "done", "branch": "claude/issue-15943-showcase-org-admin-position", "pr": "https://github.com/objectstack-ai/objectstack/pull/18658", "session_id": "session_017ef78bLdybu3AffehKkhfk", "report_comment": "(this comment)", "premise_still_valid": true, "summary": "Re-took the location by CONTENT on tree objectstack-ai/objectstack at BASE cf39b83c0: the predicate is at :94 (the card's :85 was true at filing) and the docblock at :24. Applied triage's option 1 only: tier.restricted.visibleWhen now reads 'org_admin' in current_user.positions, and the docblock no longer asserts a server-side behaviour that does not hold - it now also records that mapMembershipRole normalizes sys_member.role='admin' into org_admin before it reaches positions, so the raw word 'admin' is never on the axis. One file changed, 11 insertions / 5 deletions, entirely inside the dispatched file surface examples/app-showcase/. Option 2 (seeding a sys_position named 'admin') was refused by triage and is not done. No packages/**, no content/docs/**, no other example app touched.", "counts_with_controls": { "tree": "objectstack-ai/objectstack, worktree objectstack-issue-15943, BASE cf39b83c090a5642c1ce260c6bc00972650b9ffa, HEAD 63fffbf4b2a42dce03c6b833ea55c58e10d22f9e", "old_predicate_in_file_at_HEAD": 0, "new_predicate_in_file_at_HEAD": 2, "firing_control_file_reader": "grep -c Guangdong on the same file = 1 (term present, reader live)", "firing_control_diff_reader": "git diff BASE HEAD -- PATH | grep -c Guangdong = 0 while Guangdong is present in the file - the diff-scoped reader discriminates rather than matching everything", "in_diff_counts": "old predicate 2, new predicate 2, sharingModel 1 (context line, therefore NOT a valid control), Guangdong 0 (the valid control)", "tree_wide": "grep -rn 'current_user.positions' examples/ = 3 hits: the two in the fixed file plus examples/app-showcase/CHANGELOG.md:4044 (historical entry, a true positive outside the fix surface)", "files_changed_BASE_to_HEAD": 1 }, "both_directions": { "normalisation_measured_not_assumed": "mapMembershipRole read from the BUILT artifact packages/spec/dist/index.mjs, built in this worktree at HEAD 63fffbf4b: admin -> org_admin, owner -> org_owner, member -> org_member, sales_manager -> sales_manager. CONTROL: the literal string 'admin' is never produced for the three membership roles (false).", "harness": "Both legs import a REAL object-definition file from this tree - AFTER is the fixed file on HEAD, BEFORE is the BASE blob written out verbatim with one export binding renamed - and drive them through the real objectql rule-validator (evaluateValidationRules) from its built dist. The predicate text is read off the imported metadata, not retyped: AFTER \"'org_admin' in current_user.positions\", BEFORE \"'admin' in current_user.positions\".", "direction_1_admin": "sys_member.role='admin' -> positions ['org_admin','everyone'] -> tier='restricted' ACCEPTED (no throw). The option is offered and the rule-validator agrees: the sentence the docblock has been asserting all along is now true.", "direction_2_firing_control_non_admin": "sys_member.role='member' -> positions ['org_member','everyone'] -> tier='restricted' REJECTED, ValidationError: \"Tier: option 'restricted' is not available\". The green was NOT bought by loosening the predicate.", "before_leg": "BEFORE rejects BOTH the admin and the non-admin - the pre-fix predicate named a name never on the axis, exactly as the card states.", "harness_control": "ungated tier='standard' stays ACCEPTED for the non-admin in BOTH legs, so the harness is not refusing everything.", "landing_and_restore": "BEFORE-leg landing proven on disk: git hash-object of the written file == the BASE blob's sha; old-predicate lines 2, new-predicate lines 0, control term Guangdong 1. Removal proven by OBSERVED STATE - file absent, git status --porcelain empty, git diff HEAD empty - never by an exit code. Runner carries an EXIT INT TERM trap with absolute paths. No test file left behind.", "third_acceptance_clause": "Not triggered: the two sides are consistent after the change, so there was nothing to stop and report on, and neither the seed nor the validator was touched." }, "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 35 families, derived on this checkout at commit 63fffbf4b, --repo assertion held against this checkout's origin remote.", "reconciliation": "OK - 'dispatch-gates --ran: 35 derived famil(ies) accounted for - 34 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).' Exit codes were captured BEFORE any pipe and recorded in the documented ':: exit N' idiom.", "green": 33, "non_zero": [ "pnpm check:dual-build-cjs-loads -> exit 3 = PREREQUISITE NOT MET (reads built output; this worktree built only the showcase's dependency closure, not every package). NOT MEASURED, not a failure.", "pnpm check:cross-package-test-inputs -> exit 1. PRE-EXISTING, not this diff: the finding names packages/cli/test/init-created-files-summary.e2e.test.ts descending packages/spec/dist/ with no declared glob reaching inside it. ABLATION: with the BASE content of the one changed file restored on disk (landing proven by git hash-object == BASE blob), the gate fails identically; restore proven by hash equality with the HEAD blob plus an empty git status. Note also that the gate pulled the @objectstack/cli scan in because of scripts/sync-template-versions.mjs in the range between my BASE and the CURRENT shared origin/main (99fcb4ac), which a sibling advanced - not because of my diff." ], "changeset_pending_families": "dispatch-gates reports 13 further families that apply once a changeset exists; this card carries none by measurement (skip-changeset), so they stay outside the derivation.", "not_owed_by_this_seat": "CI convergence. The 52 artifact-roster families, the 11 declared-wide-population families, the unreachable listing, the 5 path-scheduled CI jobs and the always-runs tail are each outside the derived total." }, "tests": { "dependency_closure_build": "pnpm --workspace-concurrency=2 --filter '@objectstack/example-showcase^...' build, under scripts/pm/os-verify-lock.sh (slot issue-15943-dev): VERDICT command-exit 0, held 474s. A first attempt spelled '--concurrency=2' after the --filter; pnpm forwarded it into the child shell script ('... fi --concurrency=2' -> 'sh: 1: Syntax error: word unexpected', exit 2). That is a runner error, not a gate finding; re-run with the flag in front of --filter and it is green.", "affected_package": "pnpm --filter @objectstack/example-showcase typecheck && pnpm --filter @objectstack/example-showcase test, under the same lock: VERDICT command-exit 0. Test Files 29 passed (29), Tests 381 passed (381).", "repo_wide_lint": "pnpm lint (eslint . --no-inline-config, the repo's only style authority) run to completion in the background at HEAD 63fffbf4b: LINT_EXIT=0. This is the UNION, so no narrowing evidence is owed.", "nul_and_control_bytes": "pnpm check:nul-bytes exit 0, plus a self-scan of the edited file with grep -naP over the control-byte class: no hits. The PR body and this comment were scanned the same way before sending.", "ablation_discipline": "Both the BEFORE-leg measurement and the gate-provenance ablation mutate, prove the mutation landed on disk by hash and by anchored counts, run, then restore and prove the restore by observed state. Neither reads an editor's or a cleanup step's exit code as proof." }, "changeset": { "decision": "skip-changeset (label applied to the PR)", "criterion": "Does anything PUBLISHED move - each manifest's files[] shipped content?", "subject": "examples/app-showcase/package.json: private = true, and it declares no files[] at all.", "firing_controls": "Same reader, same run: packages/spec - private unset, files = [dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json]; packages/lint - private unset, files = [dist, README.md, CHANGELOG.md], i.e. it really does ship its CHANGELOG.md. The reader discriminates.", "corroboration": "pnpm check:published-files exit 0 on this diff. Precedent on this tree: the most recent showcase-only landing, cac62d5f9 (#17514), carried no changeset either. Note .changeset/config.json sets privatePackages {version: true, tag: false}, so a changeset here would version a private package and publish nothing." }, "api_writes": { "count": 5, "endpoints": [ "git push -u origin claude/issue-15943-showcase-org-admin-position (empty-branch write/routing probe, exit 0, no 403)", "git push origin claude/issue-15943-showcase-org-admin-position (the fix commit; pre-push printed 'check:commit-card-trailers: 1 commit message(s) ... no card relation and no model identifier in the trailer pair')", "POST /repos/objectstack-ai/objectstack/pulls (draft PR 18658)", "POST /repos/objectstack-ai/objectstack/issues/18658/labels (additive, skip-changeset)", "POST /repos/objectstack-ai/objectstack/issues/15943/comments (this report comment)" ], "note": "All REST writes went through curl with an explicit Content-Type: application/json header. No PATCH of any body. No 429 on any route. Label read-back was comparative: union(read_before={size/s}, target={skip-changeset}) == read-back {size/s, skip-changeset}, nothing stripped. Caveat recorded honestly: a read-back proves the write landed, it cannot prove any gate is reading the label. PR body was read back and is byte-identical to what was sent apart from a stripped trailing newline; exactly one attribution footer, session-URL form, under the rule line." }, "mcp_calls": 0, "open_questions": [], "out_of_scope_findings": [ "to file (3 classes, dedupe words): packages/cli/test/init-created-files-summary.e2e.test.ts descends packages/spec/dist/ and no declared glob reaches inside that radius, so pnpm check:cross-package-test-inputs reds whenever a diff pulls the @objectstack/cli scan into the run - reproduced here with BASE content on disk, i.e. independently of this PR. Class (a), a reproducible defect with a named failing probe. Dedupe words: cross-package-test-inputs, init-created-files-summary, spec/dist walk, heldBy, undeclared walk root.", "noted, not filed: the showcase's role-gated option has no pin anywhere - objectui's live spec e2e/live/cascading-options.spec.ts mentions the role gate only in prose and no repo test asserts that 'restricted' is offered to an admin. That absence is exactly why this defect sat unnoticed, but a missing pin is not itself a reproducible defect, a declared-contract violation or an authoring trap. Next person to reach it: whoever next edits this fixture or that live spec.", "noted, not filed: packages/objectql/src/validation/rule-validator.option-visibility.test.ts uses a synthetic \"'admin' in current_user.positions\" fixture. That is the generic evaluator under test with an arbitrary position name, not a claim about the identity axis, so it is correct as written; recorded only so the next reader does not mistake it for a second instance of this card.", "noted, not filed: the inline field comment just above the tier options ('Role-gated option: restricted is offered only to admins') stays accurate after the fix and was deliberately left untouched to hold this S-class card at the predicate plus its docblock. Next person to reach it: whoever next edits this fixture." ] }
Generated by Claude Code
os-try-charles commented
on Sep 17, 2026 CollaboratorMore actions落地记录 —— PR #18658 已合并,两读确认(2026-09-17T13:31Z)
读数一(队列与时间线,2026-09-17T13:31Z 取):
git ls-remote --heads origin 'claude/issue-15943*'回 0 条;时间线merged+removed_from_merge_queue。读数二(2026-09-17T13:31Z 取,重新 fetch 的
origin/main上按内容探):基线(落地前 origin/main @ 84ad2e139) 落地后(origin/main @ 30be2ac0b) org_admin 0 → 3 ← 新谓词与新档注都在 main 上了 'admin' in current_user.positions 2 → 0 ← 旧的那个名字在该文件里彻底消失 CONTROL Guangdong 1 → 1 ← in-diff 计数 0,故合格⚠️ 对照挑法:sharingModel(两端 2)与CascadingSelect(两端都读 1、看着完全没动)的 in-diff 计数各是 1 ⇒ 都被 diff 移动过,⛔ 弃用。交付的是什么
谓词
'admin'→'org_admin'(分诊裁定的选项 1),档注同笔改写并写明规范化链路:sys_member.role = 'admin'经mapMembershipRole规范成org_admin才进positions,⇒ 原始的admin从不在那条轴上,点它的谓词对所有人为假。两个方向都用真夹具测过(施工席跑的,本席核过依据):
admin → positions ['org_admin','everyone'] → tier='restricted' 被接受 非 admin → positions ['org_member','everyone'] → 被拒(ValidationError: option 'restricted' is not available) BEFORE 那条腿把 admin 与非 admin 都拒 —— 正是卡面指控的形态 夹具对照:未设门的 tier='standard' 在两条腿上都通过 ⇒ 夹具不是在拒绝一切⛔ 未新建任何
sys_position(分诊否决的选项 2)· ⛔ 未碰packages/**/content/docs/**/ 其它示例 app。⭐ 本席复核时更正的一处成因(已写在 PR 评论里,此处记要)
施工席把它遇到的
check:cross-package-test-inputsexit 1 归因为 diff 区间选择。本席三腿对照实测:判据是这棵树构建过没有(packages/spec/dist这个被 gitignore 的目录存不存在),⛔ 与区间无关;该条已碼 #18348。⇒ ⛔ 未按其框架另立卡。PR 正文写
Fixes #15943⇒ 卡自动关闭;本席同笔摘pm:dispatched、清 assignee。
Generated by Claude Code
- added a commit that references this issue
on Sep 28, 2026
现象
examples/app-showcase/src/data/objects/cascading-select.object.ts:85用收敛
tier字段的restricted选项,且该文件的文档注释(:23-27)声称:但
'admin'不是服务端positions轴上的名字。实测
服务端 positions 由
resolveUserAuthzGrants派生,membership 角色经mapMembershipRole规范化后才进入positions(packages/core/src/security/resolve-authz-context.ts:821)。对着已构建的packages/spec/dist/index.mjs实测该函数:⇒ 一个
sys_member.role = 'admin'的用户,服务端positions里是org_admin,没有admin。而positions.mdx:80-90列出的内建名也只有platform_admin/org_owner/org_admin/org_member—— 同样没有admin。除非部署里恰好建了一条名为admin的sys_position行,该谓词在服务端恒为假。为什么这条独立于 #15136
方向相反,且是先前就存在的:
positions装的是 better-auth 的user.role标量,若该标量是admin,选项显示;而服务端 rule-validator 按org_admin判,提交会被拒。建议
二选一,择其一即可:
'org_admin' in current_user.positions(与内建名一致),或admin的sys_position并做真实指派 —— 但那会让 showcase 示范一个与内建名极易混淆的岗位名,不推荐。同时修正
:23-27的注释,它目前断言了一个未成立的服务端行为。出处
由 #15136 的 dev 席在做
current_user.positions全仓普查时测到,顺路发现,未在该 PR 内修复(范围外)。