Skip to content

fix(showcase): gate the restricted tier option on the built-in org_admin position - #18658

Merged
os-try-charles merged 1 commit into
mainfrom
claude/issue-15943-showcase-org-admin-position
Sep 17, 2026
Merged

os-try-charles merged 1 commit into
mainfrom
claude/issue-15943-showcase-org-admin-position

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

Fixes #15943

Clause-②: no — this narrows a predicate from a name that never matched to the one that does. Nothing is widened: the accepted set for a non-admin is unchanged (measured below), and the only principal whose verdict moves is one the docblock already claimed the option was for.

What was wrong

examples/app-showcase's cascading-select fixture gated its restricted tier option with 'admin' in current_user.positions, but admin is never on the server's positions axis. A membership row's sys_member.role = 'admin' is normalized by mapMembershipRole into the built-in identity name org_admin before it is pushed onto positions (packages/core/src/security/resolve-authz-context.ts:793-794). The predicate was therefore false for every principal server-side, while the file's own docblock asserted that the rule-validator enforces the same rule against a non-admin who submits the value anyway.

What changed

One file, two places — the predicate and the docblock that describes it:

  • tier.restricted.visibleWhen now reads 'org_admin' in current_user.positions, matching the built-in name the docs already demonstrate (content/docs/permissions/positions.mdx:90).
  • The docblock no longer asserts server-side behaviour that does not hold, and now records why the raw membership word admin never reaches the axis — the trap an author copying this fixture would otherwise inherit.

Triage's option 2 (seeding a sys_position row literally named admin) was refused and is not done here: it would make the flagship example demonstrate a position name trivially confusable with the built-in one.

Counts, each with a firing control

Tree objectstack-ai/objectstack, worktree objectstack-issue-15943, BASE cf39b83c0, HEAD 63fffbf4b.

reading count control (same reader, same tree)
'admin' in current_user.positions in the changed file at HEAD 0 Guangdong in the same file: 1 — reader live
'org_admin' in current_user.positions in the changed file at HEAD 2 (docblock + predicate) same
git diff BASE HEAD -- PATH | grep -c Guangdong 0 Guangdong is present in the file — so the diff-scoped reader discriminates rather than matching everything
grep -rn 'current_user.positions' examples/ 3 hits, all accounted for the third is examples/app-showcase/CHANGELOG.md:4044, a historical entry — a true positive outside the fix surface
files changed BASE..HEAD 1

Both directions, measured

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 - is the literal string "admin" ever produced for the three membership roles? false

Both legs then 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 from its built dist. The predicate text is read off the imported metadata, not retyped:

AFTER  tier.restricted.visibleWhen = "'org_admin' in current_user.positions"
BEFORE tier.restricted.visibleWhen = "'admin' in current_user.positions"
CONTROL, same read - AFTER province.zj.visibleWhen = "record.country == 'cn'"

positions axis, derived from mapMembershipRole, not assumed:
  sys_member.role='admin'  -> ["org_admin","everyone"]
  sys_member.role='member' -> ["org_member","everyone"]
  CONTROL - literal 'admin' anywhere on either axis: false

SERVER VERDICTS - objectql evaluateValidationRules(..., "insert"):
  BEFORE (BASE blob)   | admin      role=admin  | tier='restricted'       | REJECTED | ValidationError: "Tier: option 'restricted' is not available"
  BEFORE (BASE blob)   | NON-ADMIN  role=member | tier='restricted'       | REJECTED | ValidationError: "Tier: option 'restricted' is not available"
  BEFORE (BASE blob)   | NON-ADMIN  role=member | CONTROL tier='standard' | ACCEPTED | no throw
  AFTER  (HEAD, fixed) | admin      role=admin  | tier='restricted'       | ACCEPTED | no throw
  AFTER  (HEAD, fixed) | NON-ADMIN  role=member | tier='restricted'       | REJECTED | ValidationError: "Tier: option 'restricted' is not available"
  AFTER  (HEAD, fixed) | NON-ADMIN  role=member | CONTROL tier='standard' | ACCEPTED | no throw
  • Direction 1 — the sys_member.role = 'admin' user resolves to org_admin, the predicate is true, the option is offered and the rule-validator accepts the submit. The sentence the docblock has been asserting all along is now true.
  • Direction 2, firing control — the non-admin is still refused, with the same located ValidationError. The green was not bought by loosening the predicate.
  • The ungated standard value stays accepted for the non-admin in both legs, so the harness is not refusing everything.

The BEFORE leg's landing was proven on disk (git hash-object of the written file vs the BASE blob, old-predicate lines 2 / new-predicate lines 0, control term Guangdong 1), and its removal was proven by observed state — file absent, git status --porcelain empty, git diff HEAD empty — never by an exit code. The runner carries an EXIT INT TERM trap with absolute paths. No test file is left behind: this is a one-off measurement, not a shipped fixture.

Verification

  • Dependency closure buildpnpm --workspace-concurrency=2 --filter '@objectstack/example-showcase^...' build under the shared verify lock: VERDICT command-exit 0.
  • Affected packagepnpm --filter @objectstack/example-showcase typecheck && ... test under the lock: VERDICT command-exit 0; 29 test files, 381 tests passed.
  • Derived gate familiesdispatch-gates --commands --repo objectstack-ai/objectstack derived 35; all 35 were run with exit codes recorded before any pipe, and reconciled: ✓ dispatch-gates --ran: 35 derived famil(ies) accounted for — 34 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3). 33 green. The two non-zero readings are not this diff:
    • pnpm check:dual-build-cjs-loads exit 3 = PREREQUISITE NOT MET — it reads built output and this worktree built only the showcase's dependency closure, not every package. Nothing was measured; recorded as such, not as a failure.
    • pnpm check:cross-package-test-inputs exit 1, and the finding names packages/cli/test/init-created-files-summary.e2e.test.ts descending packages/spec/dist/ — a file this diff does not touch. Proven pre-existing by ablation: with the BASE content of the one changed file restored on disk (landing proven by git hash-object equality with the BASE blob), the gate fails identically. Restore proven by hash equality with the HEAD blob plus an empty git status. See the acceptance note below.

Changeset — measured, skip-changeset

The one criterion is whether anything published moves.

  • SUBJECT examples/app-showcase/package.json: private: true, and it declares no files[] at all.
  • FIRING CONTROLS, same reader, same run: packages/specprivate unset, files = ["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json"]; packages/lintprivate unset, files = ["dist","README.md","CHANGELOG.md"] (it does ship its CHANGELOG.md). The reader discriminates.
  • pnpm check:published-files exit 0 on this diff.
  • Precedent on this tree: the most recent showcase-only landing, cac62d5f9 ("add the loop { parallel } fixture", test(showcase): add the loop { parallel } fixture that unblocks the flow-run-step-nesting clause #17514), carried no changeset either.

Nothing in any manifest's files[] moves, so this PR takes the skip-changeset label rather than an empty changeset.

Acceptance notes (out of scope, not filed from this seat)

  • To filepackages/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 at BASE content, i.e. independently of this PR. 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: the objectui 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 contract violation or an authoring trap. Next person to reach it: whoever next edits this fixture or that live spec.
  • Noted, not filedpackages/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.

Sibling card #15136 remains open and is untouched here; this fix is independent of it in both directions.


Generated by Claude Code

…min position

The cascading-select fixture gated its `restricted` tier option with
`'admin' in current_user.positions`, but `admin` is never on the server's
positions axis: a membership row's `sys_member.role = 'admin'` is normalized by
`mapMembershipRole` into the built-in identity name `org_admin` before it is
pushed onto `positions`. The predicate was therefore false for every principal
server-side, while the file's own docblock asserted that the rule-validator
enforces the same rule against a non-admin submitter.

Name the built-in position instead, and correct the docblock so it no longer
asserts server-side behaviour that does not hold — it now also records why the
raw membership word `admin` never reaches the axis, which is the trap an author
copying this fixture would otherwise inherit.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
@os-try-charles os-try-charles added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

席内复核(派发席 domain:devx):形态与围栏 PASS;⭐ 一处框架更正,凭三腿对照

读数时点 2026-09-17T12:50Z;PR 头 63fffbf4b(具名 ref pr18658),对照 origin/main @ 62d830e54

形态与围栏

1 个文件 · +11/−5 · examples/app-showcase/src/data/objects/cascading-select.object.ts
PR 正文行首 `Fixes #15943` 与 `Clause-②: no`
谓词:'admin' → 'org_admin' —— ⛔ 正是分诊裁定的选项 1
档注:不再断言一个服务端不成立的行为,并写明 mapMembershipRole 把 sys_member.role='admin' 规范成 org_admin
⛔ 未碰 packages/** · ⛔ 未碰 content/docs/** · ⛔ 未新建任何 sys_position(分诊否决的选项 2)

⭐ 更正:那条红的成因不是施工席给的那个

施工席报 pnpm check:cross-package-test-inputs exit 1,并归因为「diff 区间里混入了 scripts/sync-template-versions.mjs,把 @objectstack/cli 的扫描拉了进来」。⚠️ 本席实测:那不是成因。

在 origin/main 的一次性 worktree 上,三腿对照(2026-09-17T12:50Z):
  无 packages/spec/dist                  EXIT 0
  mkdir -p packages/spec/dist(空目录)   EXIT 1   ← 一个被 gitignore 的空目录就够
  rmdir packages/spec/dist               EXIT 0
  三腿跑完 git status 仍 0 行

同一读法:PR 头的干净 worktree(未构建)EXIT 0;origin/main 未构建 EXIT 0

⇒ 真正的判据是这棵树构建过没有,⛔ 不是 diff 区间里有什么。施工席的 worktree 跑过 showcase 依赖闭包构建 ⇒ packages/spec/dist 存在 ⇒ 红;本席的探针树没构建 ⇒ 绿;CI 绿是因为那一步跑在构建之前。

⭐ 而这条已经有卡:#18348(已关闭)逐字记的就是它 ——「裁决取决于被 gitignore 的空目录 packages/spec/dist 存不存在;构建过的工作树一律红,CI 绿只因那一步跑在构建之前」。⇒ ⛔ 本席不按施工席的框架另立卡,那会把下一个人送去查一个与成因无关的区间选择。⛔ 也不因此拦这个 PR:它与本 diff 无关,两个方向都测过。

施工席做对、且本席核过依据的两件

  1. 两个方向都用真夹具测的,⛔ 不是断言:admin ⇒ positions ['org_admin','everyone']tier='restricted' 被接受;非 admin ⇒ ['org_member','everyone']被拒(ValidationError: option 'restricted' is not available)。发火对照:未设门的 tier='standard' 在两条腿上都通过 ⇒ 夹具不是在拒绝一切。BEFORE 那条腿把 admin 与非 admin 都拒,正是卡面指控的形态。
  2. 对照是按 in-diff 计数挑的:它自己记下 sharingModel 两端相等但落在 hunk 里 ⇒ 判为不合格,改用 Guangdong(in-diff 0、文件内存在)。

放行路径

绿判据 2026-09-17T12:50Z 读为 NOT GREEN(6 条 in_progress)⇒ 等;绿后翻 ready → 重读到 total_count 真的移动 → 武装。


Generated by Claude Code

@os-try-charles
os-try-charles marked this pull request as ready for review September 17, 2026 13:07
@os-try-charles
os-try-charles added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 30be2ac Sep 17, 2026
39 checks passed
@os-try-charles
os-try-charles deleted the claude/issue-15943-showcase-org-admin-position branch September 17, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants