Conversation
…d mark its fence
The `usePermissions` hook example in `skills/objectui/guides/auth-permissions.md`
gated two buttons on `check('contacts', 'update', contact)`. `check` answers a
`PermissionCheckResult` object, an object is truthy, and so both buttons rendered
for every user, denied ones included. The fence was unmarked, so
`check-skill-examples` never compiled it.
The example now gates on `can('contacts', 'update')` / `can('contacts', 'delete')`
— the one boolean spelling the guide's neighbouring paragraph already publishes —
annotated `: boolean` so a `check(...)` put back in that position fails to compile.
The record argument is dropped: measured against the built `@object-ui/permissions`
dist with a throwing Proxy as the record, `evaluatePermission` performs zero
property reads on it, and `can()` agrees with `check(..., record).allowed` on every
(role, action) of the guide's own config.
The fence carries the `os:check` marker and is tagged `tsx`, imports `Button`
from `@object-ui/components`, and types `contact` inline, so the gate compiles
it against the built dist from now on (Semantic phase: 16 of 16 ts fences).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2
Contract reviewServed-tier: In-seat review by the ① Derived judgments
② Semver levelNone owed — ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #9671
Clause-②: yes
Draft against
main.skills/**is a governed surface (Tier H): this PR stays draft until an authorized approval, and the seat lands it. Dev run under thedomain:skillsseat (objectstack#7623), sessionhttps://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2.What changed
One file:
skills/objectui/guides/auth-permissions.md, theusePermissionshook example under the heading "usePermissions hook".can('contacts', 'update')/can('contacts', 'delete')— a boolean, and the one spelling the guide's neighbouring paragraph ("Publishcan(...)") already publishes.check(...)is gone from the example: it answers aPermissionCheckResultobject, an object is truthy, and so gated on it both buttons rendered for a denied user.: boolean. That annotation is the tripwire: acheck(...)put back in that position no longer compiles (TS2322), so the next instance of this defect class goes red in the gate instead of shipping (ablation leg A below).os:checkHTML-comment marker on the line directly above it and is taggedtsx, soscripts/check-skill-examples.mjscompiles it against the built dist from now on. To compile it:Buttonis imported from@object-ui/components, andcontactis typed inline as an object with an optional numericsalary.checkField(...)is untouched (it already answers a boolean).contactargument is dropped from the gates — see the API-shape decision.API-shape decision:
can(object, action), record droppedcantakes no record; the old example passedcontacttocheck. Measured against the BUILTpackages/permissions/dist/index.js, with a throwing Proxy handed in asrecord(anyget/has/ownKeys/ descriptor read on it throws and is counted), over the guide's ownPermissionProviderconfig:can()(no record)check(..., record).allowedrecord property accesses observed: 0—evaluatePermission(packages/permissions/src/evaluator.ts) never reads a field of the record. Its only use ofrecordis presence: when a record is passed AND the role carriesrowPermissions, the role's grant additionally requires some row rule'sactionsto list the action.evaluateConditionin the same file has no caller on thecheckpath (its callers are tests and other packages' own evaluators).deletewhose row rules list onlyreadanswerstruewithout a record andfalsewith one — still with zero reads of the record. That is a per-role static toggle, not record-level gating; the guide's own section "Row filters are returned verbatim — nothing interpolates them" already says the package evaluates no row filter client-side. An example passingcontacttherefore taught record-level gating the package does not perform; the boolean spelling that keeps the example true iscan(...)with no record.check(...).allowed(a second spelling for the same question — the drift the card names), and any new helper or package API change (the repair is in the guide).Line readings (the two the skills rule requires)
c255b38, unchanged atedbcf1e)48d3d2b)skills/objectui/guides/auth-permissions.mdskills/objectui/**/*.mdsummed(
wc -l;origin/mainmoved fromc255b38toedbcf1ebetween dispatch and branch-out with both readings unchanged.)Marked population
At
48d3d2bthe gate printsMarked: 16 ts fence(s) (floor 13), 70 json fence(s) (floor 70);--listshows the new rowskills/objectui/guides/auth-permissions.md:225 [tsx] marked pass. Before: the BASE tree carries 85os:checkmarkers underskills/plus.claude/skills/and this branch 86 (git grep -con the BASE ref vs the worktree — the unbuilt BASE tree cannot print the gate's own line), so ts 15 → 16, json 70 → 70.MARKED_FLOORinscripts/check-skill-examples.mjsis left atts: 13. Its header invites raising it in the PR that adds a marker; that file is outside this card's claimed file surface (the guide, and the eval JSON only if a gate demanded it), so it is not touched here — see Acceptance notes.Gates (exit captured before any pipe; verdict lines quoted)
Closure build first, under objectstack's verification lock:
pnpm exec turbo run build $(node scripts/check-skill-examples.mjs --build-filter) --concurrency=2→Tasks: 29 successful, 29 total· lockVERDICT command-exit 0 · held the lock 217s.pnpm check:skill-examplesSemantic phase: 16 of 16 ts fence(s) judged, 0 failed.·Every marked skill example holds up against the built types.pnpm check:skill-eval-tokensEvery must_contain token is taught by its own skill bundle.(eval JSON untouched)pnpm check:skills-pathscheck-skills-paths: OK (88/89 stated path(s) resolve across 20 guide file(s); 1 baselined).pnpm check:new-line-citationsVERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0pnpm check:control-bytescheck-control-bytes: OK (scanned 8061 tracked text file(s); skipped 85 binary).node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/auth-permissions.mdGOVERNED — 1 of 1 path(s) are on a governed surface: skills/** x1(informational; the expected reading)node scripts/check-changeset-presence.mjsNo source or published contract of a released package changed in this range, so no changeset is owed.— no changeset addedpnpm exec vitest run packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx(the testmarkdown-test-inputs.mjs --changednames for this guide)Test Files 1 passed (1)·Tests 8 passed (8)pnpm exec vitest run scripts/__tests__/check-skill-examples.test.tsTest Files 1 passed (1)·Tests 113 passed (113)NOT MEASURED locally:
pnpm lint(repo-levelturbo run lint, CI's run). Reason it was not run, not a measurement: the diff is one.mdfile andeslint.config.jsconfigures no markdown processor.Ablation
From the committed state
48d3d2b, each leg through objectstack'sscripts/ablation-replace.mjs: mutation proven by anchor counts and blob hash, restore proven by blob == HEAD blob (b9f4672) and an emptygit diff HEAD, tree clean after each leg.checkre-added to the destructure andcanEdit: boolean = check('contacts', 'update', contact). Gate exit 1:[semantic] skills/objectui/guides/auth-permissions.md:236:9 TS2322: Type 'PermissionCheckResult' is not assignable to type 'boolean'.·Semantic phase: 16 of 16 ts fence(s) judged, 1 failed.Direction: red, as predicted.Marked: 15 ts fence(s) (floor 13),Semantic phase: 15 of 15 ts fence(s) judged, 0 failed,Every marked skill example holds up. That is the blind spot the card names, reproduced: unmarked, the defect is invisible to the only instrument that can see it. It also shows the floor at 13 does not red on this fence being unmarked (16 → 15), which is why the floor note above exists.x1 → x1), restored, and exited 1 before the gate; the leg was re-run with a distinct replacement. Recorded so the count of runs is honest.Acceptance notes
MARKED_FLOORtsstays 13 with 16 marked; the gate header's invited raise is outside this card's file surface. 承接者: the landing seat (a one-line follow-up inscripts/check-skill-examples.mjs), or none if the seat prefers the floor to move only with a census.actionslist and never the filter or the record (measured above). Different paragraph, outside this card's section; prose precision, not a copied-code defect. 承接者: none identified.dataSourceexpression root #9378 (objectui#9370) holds three other guides of the package and is not touched.needs:contract-reviewis the seat's to hang on this PR.维护者速读(草稿)
改了什么 —
skills/objectui/guides/auth-permissions.md里usePermissions那段示例:两个按钮的显隐改为看can('contacts', 'update')/can('contacts', 'delete')(布尔值),不再看check(...)(一个对象,恒为真);示例围栏加了os:check标记并改为tsx,补上编译所需的Button导入与contact类型;去掉了传给check的contact参数。为什么改 — 这段代码会被客户项目照抄。原写法下被拒绝的用户也能看到「编辑」「删除」按钮,权限门形同虚设;而该围栏此前未被任何门禁编译,所以没人发现。实测(对着已构建的
@object-ui/permissions)check从不读取记录的任何字段,can在指南自己的配置上与check(..., record).allowed逐项一致,所以去掉记录参数不改变示例演示的任何结论。风险与代价(含回滚) — 只改一个已发布技能文件(净 +6 行,在 PM 预算内),不动任何包源码、不动 API,不需要 changeset。回滚 = revert 这一个 commit。留下的一处:门禁的
MARKED_FLOOR仍是 13(现有 16 个标记围栏),要不要同步抬到 16 交席位决定。席位意见 —
你要做的 — 受管面 PR,需要一条获授权账号的 APPROVED review;批准后由席位入队落地。无需其他动作。
Generated by Claude Code