docs(skills): read the published positions in the PermissionProvider example, and let the compiler hold that fence - #8502
Conversation
…r example, and let the compiler hold that fence
The `PermissionProvider` wiring example in the auth-permissions guide passed
`userRoles={user?.roles || ['viewer']}`. `AuthUser` has had no `roles` member
since objectui#5424 (framework ADR-0090 D3 renamed it to `positions` with no
deprecation window), so that read resolves through the type's
`[key: string]: unknown` index signature and a reader copying the line into a
typed app gets TS2740 on it.
Two changes, and the second is why the first cannot come back:
- the read becomes `user?.positions`, the published spelling;
- the fence carries the `check:skill-examples` opt-in marker, so the gate now
compiles it against the BUILT `dist/*.d.ts` on every run.
Marking cost three added lines — the marker, a `useAuth` import the fence used
without importing, and a one-line stub for the `AppContent` placeholder — and
surfaced one more copy-then-compile failure on the same fence: `user={user}`
handed `AuthUser | null` to a prop typed `{ id: string; ... } | undefined`
(TS2322 at the fence's `user=` line). Fixed in place as `user={user ?? undefined}`,
same defect class, no net line cost.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY
维护者速读(终稿 · skills 席,2026-09-08T03:24Z)改了什么 — objectui 权限指南里 为什么改 — 这段示例是 AI 写 ObjectUI 权限接线时抄的第一份材料。 风险与代价(含回滚) — 只动一份发布给使用者的 markdown 指南,不碰运行时代码;changeset 门禁判定不欠。席位复核:diff 与认领申报的文件面一致(1 个文件);两处改动都是向已发布契约收窄,没有任何兼容别名;消融已证门禁会因退役写法变红(exit 1 点名该行,恢复后 exit 0)。一个代价值得你看一眼: 席位意见 — ACCEPT。两处收窄都对准已发布契约,门禁标记让同类回归以后由机器拦住,+3 行买到的是一段真正能编译的示例。顺带记一笔不在本卡的观察: 你要做的 — 受管面( Generated by Claude Code |
Fixes #8393
The
PermissionProviderwiring example inskills/objectui/guides/auth-permissions.mdpasseduserRoles={user?.roles || ['viewer']}.AuthUserhas had norolesmember since objectui#5424 (framework ADR-0090 D3 renamed it topositionswith no deprecation window), so that read resolves through the type's index signature asunknownand a reader copying the line into a typed app gets a TS2740.Two changes. The second is why the first cannot come back.
user?.positions— the published spelling. No compatibility alias, no shadow of the retired member;packages/auth/src/types.tsforbids both.check:skill-examplesopt-in marker (the HTML-comment marker whose spelling lives inMARKERinscripts/check-skill-examples.mjs), so the gate compiles this fence against the BUILTdist/*.d.tson every run.Measurements
The card's probe, re-run on this branch
Assumption re-measured, not inherited. Package closure built first —
turbo run buildover the gate's own--build-filterset, 29 tasks, exit 0 — then two lines compiled against the freshly builtpackages/auth/dist/index.d.ts, one of them a control:Exactly one diagnostic, on the guide's line, with the control line clean:
The instrument was lit and
positionsis the working spelling. (The card measured "and 28 more" on its own probe; the count moves with the TypeScript lib version and is not load-bearing.)Why this fence, and what marking cost
The card left the marking decision open.
node scripts/check-skill-examples.mjs --measurejudged the fence unmarked and named exactly what stood between it and green — twoTS2304lines, both about names the fence used without introducing:Three added lines close both and the gate goes green, so the fence is marked: the marker line,
import { useAuth } from '@object-ui/auth';, anddeclare function AppContent(): null;as a one-line stand-in for the reader's own app tree. No example prose was widened to make the gate happy.Marking then surfaced one more copy-then-compile failure on the same fence, which only the compiler could have found:
useAuth()hands backAuthUser | null;PermissionProviderProps.user(packages/permissions/src/PermissionProvider.tsx:29) is optional but not nullable. Repaired in place asuser={user ?? undefined}— same defect class as the card (an example that fails on copy), mechanical, one modified line, zero net lines, inside this claim's declared file surface, and inside the same gate family. It is not a lenient consumer-side alias: nothing is renamed or coerced, and no second dialect is created.The mark is real — ablation, both legs proven on disk
One-shot proof, run from the committed tree, restore leg pointed at
HEAD, trap armed with absolute paths:user?.positions1 to 0,user?.roles0 to 1; blobe8b4c065differs from HEAD blobc38c16c7Semantic phase: 14 of 14 ts fence(s) judged, 1 failed.with[semantic] skills/objectui/guides/auth-permissions.md:192:7 TS2740: ...c38c16c7= the HEAD blob,git diff HEADemptySemantic phase: 14 of 14 ts fence(s) judged, 0 failed./Every marked skill example holds up against the built types.So the repository's own gate now produces, unaided, the diagnostic the card had to build a standalone probe for. Marked
tspopulation 13 to 14 (floor 13 — growth is the direction the floor is shaped to allow).The runtime half — measured in-repo, and what that reading is not
The card attributed this half to objectui#5424 rather than measuring it, and asked whoever picked it up not to inherit it. There is an in-repo reading:
packages/auth/src/__tests__/workspaceAdminPositions-5389.test.tsx:77holds the verbatimuserobject returned byGET /api/v1/auth/get-sessionon a real single-tenant server (published@objectstack/cli/@objectstack/plugin-auth17.1.0), and lines 188 to 195 pin it:That suite runs green on this branch (193 tests across the four files run).⚠️ Read it for what it is: a captured payload held by a pin, not a live server booted for this PR. It is stronger than the card's attribution and weaker than a fresh live capture. Consequence for a reader who silences the type error instead of changing the spelling: the retired key is absent on the wire, so
user?.roles || ['viewer']yields['viewer']for every signed-in user unconditionally — a permission example that grants the narrowest role to everyone while looking wired.Line budget
Both readings taken at
HEADafter the final commit,2f2dbe4.67299bf)skills/objectui/guides/auth-permissions.mdskills/)The before figure for the package was measured on a
git archiveof the base commit, not derived by subtraction; it agrees with the 4584 recorded after objectui PR #8427. Budget for this card was +4 on each; the net cost is the marker plus the two lines the marker needs, and the two content edits are line-neutral.Gates
Run locally on this branch, exit code captured before any pipe, verdict lines quoted from each gate's own output.
pnpm check:skill-examplesEvery marked skill example holds up against the built types.node scripts/check-skill-examples.mjs --self-test✓ check-skill-examples self-test: 59 cases passpnpm check:skill-eval-tokensEvery must_contain token is taught by its own skill bundle.pnpm check:skills-paths✅ check-skills-paths: OK (88/89 stated path(s) resolve across 20 guide file(s); 1 baselined).pnpm check:control-bytes✅ check-control-bytes: OK (scanned 6719 tracked text file(s); skipped 85 binary).pnpm exec vitest runover the three skills-gate suites plusworkspaceAdminPositions-5389.test.tsxTest Files 4 passed (4) / Tests 193 passed (193)pnpm type-check:scriptstsc -p tsconfig.scripts.jsonpnpm lint:root✖ 32 problems (0 errors, 32 warnings)— all 32 pre-existing, none in this diffnode scripts/check-changeset-presence.mjs✅ No source or published contract of a released package changed in this range, so no changeset is owed.node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/auth-permissions.md⛔ GOVERNED — 1 of 1 path(s) are on a governed surface: skills/** x1Changeset: none. The gate above is the authority in this repository and it says none is owed — one markdown file under
skills/, no published source and no manifest contract moved.pnpm lint:rootis a narrowed run and the narrowing is measured, not assumed: eslint's own config restricts every block tofiles: ['**/*.{ts,tsx}']with no markdown processor configured, this diff contains zero.ts/.tsxfiles, and no type-aware rule is wired that could let a markdown edit move any TypeScript file's verdict. The repo-wide farm belongs to CI.Local trial merge against
origin/mainat67299bf:git merge-tree --write-treeexits 0 and writes treef6d05dee— no conflicts.维护者速读(草稿)
席位起草,维护者定稿。
改了什么 — 权限指南里那段
PermissionProvider接线示例,原来读的是user.roles。这个成员在 objectui#5424 已经退役(框架 ADR-0090 D3 把它改名positions,没有过渡期)。示例改成读positions;同时给这段示例挂上编译门禁的标记,让机器以后替我们盯着它。挂标记顺带暴露了同一段示例里第二处照抄即报错的地方(把可能为 null 的 user 传给一个不接受 null 的属性),一并就地修好。三行净增:一行标记、一行示例本来就漏掉的 import、一行占位组件声明。为什么改 — 指南是 AI 写 ObjectUI 代码时读的第一份材料。这一行不是「写得不够好」,是照抄进 TypeScript 工程里直接编译不过;真要绕过编译错误硬跑,后果更糟:退役的键在会话接口上根本不存在,于是每个登录用户都被判成最低权限的 viewer,而界面看上去是接好的。它能活到今天,正因为这段示例没被任何门禁编译过 —— 所以这次不只改字,还把它纳入门禁。
风险与代价(含回滚) — 风险很低:改的是一份发布给使用者的 markdown 指南,不是运行时代码,没有任何已发布包的源码或发布契约移动(changeset 门禁判定为不欠)。唯一的持续代价是这段示例从此被编译门禁盯着,以后改它要保持能编译 —— 这正是我们要的。回滚就是 revert 这一个提交,没有数据迁移、没有版本联动。已用消融实验证明门禁真的会因为这一行变红(把退役写法放回去,门禁 exit 1 并点名该行),不是挂了个不干活的标记。
席位意见 — (留空,待维护者/评审席位填写)
你要做的 — 这是受管面(发布给使用者的
skills/**),按规程停在 draft 等你合并;你的那次合并动作本身就是审核记录。请看两点:①positions是不是你要的最终写法(而不是任何形式的兼容别名);② 给这段示例挂编译门禁的标记是否合意 —— 代价是三行,收益是同类错误以后由机器拦住。Generated by Claude Code