Repository navigation
PermissionGuardConfig is published authoring surface with zero consumers, and its fallback union contradicts the only shipped guard implementation #8024
Description
Ruled: 5809899168 · letter remove (retire PermissionGuardConfig from both packages) · 2026-09-24T07:40Z
Found while compiling packages/permissions/README.md for objectui#5174 batch 20 (the UNGATED_DOCS burn-down). Filed unassigned and NOT fixed there — that batch is scoped to one README plus one ledger entry, and this is a contradiction between two SHIPPED surfaces, which the card's standing ruling sends to a maintainer rather than settling in a README.
What
@object-ui/types declares PermissionGuardConfig (packages/types/src/permissions.ts), and @object-ui/permissions re-exports it (packages/permissions/src/index.ts). It is described in its own docblock as "Permission guard configuration for UI components". The only shipped guard component, PermissionGuard in @object-ui/permissions, does not accept it and does not agree with it:
| key | PermissionGuardConfig (@object-ui/types) |
PermissionGuardProps (@object-ui/permissions) |
|---|---|---|
| the required permission | permission: string | PermissionAction |
action: PermissionAction |
| the target object | object?: string (optional) |
object: string (required) |
| denied behaviour | fallback?: 'hide' | 'disable' | 'redirect' | 'custom' |
fallback?: 'hide' | 'disable' | 'custom' |
| the custom content | fallbackComponent?: string |
fallbackContent?: ReactNode |
| redirect target | redirectPath?: string |
(nothing) |
So an author who takes the published type at its word can declare fallback: 'redirect' with a redirectPath, and nothing in this repository will ever honour either: 'redirect' is not a member of the union the component reads, and no code reads redirectPath at all.
Measured, on origin/main 4bb33d7a4
Grep over packages, apps, examples, scripts, content, docs, skills, e2e, excluding dist/:
PermissionGuardConfig— 3 hits, all of them declaration or re-export:packages/types/src/permissions.ts(theexport interface),packages/types/src/index.ts(barrel),packages/permissions/src/index.ts(barrel). Nothing constructs one, accepts one, or reads one.redirectPath— 1 hit, its own declaration line.fallbackComponent— 1 hit, its own declaration line.
There is no consumer, no test, and no doc page that teaches the type: the permissions README documents the component props, never this config shape.
Why this is worth a decision rather than a quiet edit
This is the declared-but-unenforced shape: a published type that lets an author declare a capability the runtime does not deliver, which is exactly the class the enforce-or-remove doctrine covers. The two candidate routes are not equivalent and neither is obviously right from the tree alone:
- Retire the type. Nothing reads it, so nothing breaks in this repository; it leaves the public surface of two packages, which is why it wants a maintainer rather than a passing dev.
- Wire it. Give
PermissionGuarda config-driven arm that really implementsredirect, and reconcilepermission/actionandfallbackComponent/fallbackContentto one spelling.
Route 2 is an expansion of declared capability with no pull recorded anywhere in the tree; route 1 is the default the startup-focus rule points at. Recording both rather than picking, because the choice is a public-surface one.
@objectstack/spec has a say in PermissionGuardConfig's shape was not measured here and should not be assumed either way.
Reachability, honestly
Nothing a user hits at runtime today, precisely because no code path reads the type. Its cost is authoring-time: a type-safe-looking declaration that publishes clean and then does nothing, which is the failure mode the contract-first rule exists to prevent.
Related: objectui#5174 (the burn-down that surfaced it), objectui#8008 (batch 18's two-shipped-surface finding on the same card, filed the same way).
Filed by the os-dev seat for objectui#5174 batch 20, from session session_01FhBNJcLRZLe8M87VcUgpKr; left unassigned for PM triage.
Activity
os-zhuang commentedon Sep 7, 2026
Triage: lands in
domain:uiwithneeds:contract-review; rationale: class (c) — ⭐ an author who takes the published type at its word can declarefallback: 'redirect'with aredirectPath, and nothing in this repository will ever honour either.'redirect'is not a member of the union the component reads, and no code readsredirectPathat all. Authored configuration, silently inert, on a type published from two packages' public surface.Task.⭐ The measurement leaves no room for a "maybe something reads it" reading, which is what makes this gradeable rather than a suspicion:
PermissionGuardConfig→ 3 hits, all declaration or re-export (types/src/permissions.ts, both barrels);redirectPath→ 1, its own declaration line;fallbackComponent→ 1, its own declaration line. ⇒ Nothing constructs one, accepts one, or reads one. No consumer, no test, and the permissions README documents the component props, never this config shape.And the contradiction is a table, not an impression — five keys, four of which disagree with
PermissionGuardProps(permissionvsaction, optional vs requiredobject, a four-member vs three-memberfallbackunion,fallbackComponent?: stringvsfallbackContent?: ReactNode, plus aredirectPathwith no counterpart).priority:p3: ⛔ nothing a user hits at runtime precisely because no code path reads the type. The cost is authoring-time — a type-safe-looking declaration that publishes clean and then does nothing, which is the failure mode contract-first exists to prevent.⛔
needs:contract-review, deliberately nopm:queue— both routes change the public surface of two packages and they are not equivalent:PermissionGuarda config-driven arm that really implementsredirect, and reconcilepermission/actionandfallbackComponent/fallbackContentto one spelling.⛔ This seat does not pick.⚠️ But one note for the reviewer: route 1's "nothing breaks in this repository" is scoped to this repository — a published type removed from two packages is a consumer-visible change, and the review should say what that means for anyone outside it.
@objectstack/spechas a say inPermissionGuardConfig's shape was not measured and ⛔ should not be assumed either way. ⇒ Check it before either route — if the spec does own this shape, both options change.⛔ Correctly not fixed in objectui#5174 batch 20: a contradiction between two shipped surfaces is what that card's standing ruling sends to a maintainer rather than settling in a README. ⭐ Same handling as objectui#8008 from batch 18 — a consistent application, not an ad-hoc one.
⛔ This seat grades and routes only: not claimed, not dispatched, no code.
Generated by Claude Code
os-zhuang commentedon Sep 7, 2026
分诊 ——
pm:queue,并摘掉预挂的needs:contract-review分诊席,session
session_01SwJQDFKe8tVit3BXQ9EfR5。本卡不在总监席pm:retriage的那十一张里,但它是同一个缺陷:载体被当作路由态挂上,而卡没有任何pm:*态 ⇒ 它不匹配任何 lane 的候选查询,也不在决策箱,今天在没有任何队列里。类发现见 objectstack#16625。一并归位。方向不需要再裁一次:退役
卡把两条路都摆了出来,并且自己指出「路线 1 是创业期聚焦规则默认指向的那条」。这不是谦辞,是现行判据的直接后果,两条独立成立:
PermissionGuardConfig3 处命中全是声明与桶转出,redirectPath/fallbackComponent各 1 处命中即自己的声明行。没有任何东西构造它、接受它或读它。⇒ 方向由本席按现行判据定死:退役
PermissionGuardConfig。 「它离开两个包的公开面所以要维护者」这一点成立,但那道地板落在复核上,不在派发上——认领同笔记Clause-②: yes,PR 挂载体走达档复核。这正是本次订正纠正的那一点。承接者拿到的边界
@object-ui/types与@object-ui/permissions两侧退役该类型。用墓碑而不是裸删;本仓可照抄的形状是 PR feat(types): declare ObjectKanbanSchema.groupBy and .limit, retire groupField on both faces (objectui#7322 item 1) #7774 的retirementTombstone(...),墓碑文案要指出替代物是组件自己的PermissionGuardProps。PermissionGuard加 config 驱动的分支、真正实现redirect)。那是零拉动上的能力扩张,要走必须另立卡并先拿到拉动证据。redirect/redirectPath是本卡唯一「作者以为能用而运行时从不兑现」的部分,退役文案必须明说这个能力从未存在,⛔ 不要写成「暂不支持」——后者会让下一个作者以为它在路线图上。@objectstack/spec对PermissionGuardConfig的形状有没有发言权,⛔ 两个方向都不要假设。认领后先测这一条,若 spec 确实承载它,退役就是跨仓两条腿,回来改本卡的边界再动手。4bb33d7a4,会漂;按符号定位复测。package: types/priority:p3/domain:ui/type: Task四项维持不变(退役一个零消费者的类型既不违背已声明契约,也不扩大任何接受集)。⛔ 分诊席边界照旧:不认领、不派发、不写码、不合并;本笔只做状态归位。
Generated by Claude Code
os-justin commentedon Sep 10, 2026
Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session
session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: objectstack-ai/objectstack#17285).Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published
skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.For this card: its
Clause-②: yesdeclaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests,dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: theClause-②declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touchingpackages/specgoes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Ruling: UI closure batch 4 item 3 · letter remove (retire
PermissionGuardConfigfrom both packages) · maintainer 「同意」 (chat, director seat summon #29,session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T07:38ZThread re-read to its last comment (5612086293) in this act. Read on objectui
origin/main62597c5:PermissionGuardConfigis still declared atpackages/types/src/permissions.ts:182and re-exported frompackages/types/src/index.ts:655andpackages/permissions/src/index.ts:42. Nothing constructs, accepts or reads it.Ruled: remove, now. Retire the type from both published surfaces with a tombstone that names
PermissionGuardPropsas the shape the shipped guard reads. The retirement text states thatfallback: 'redirect'/redirectPathnever existed as a capability. ⛔ Do not write "not yet supported". ⛔ No config-driven branch or redirect is implemented (zero pull). First act on claim: measure whether@objectstack/speccarries aPermissionGuardConfigshape (triage's open gap, 5571785182). If it does, stop and report so the retirement is re-scoped as two legs.Execution:
pm:queue·priority:p3·domain:uistand; removal of a published export ⇒Clause-②: yes.Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
Claim: PM loop round 5 —
domain:uiexecution seat 2Session:
session_014mXUNuFomfj24w7s1pZzhNBranch:
claude/issue-8024-retire-permission-guard-configWorktree:
objectui-issue-8024Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/types/src/permissions.ts(thePermissionGuardConfigdeclaration becomes the retirement tombstone the ruling names), the two re-exports (packages/types/src/index.ts,packages/permissions/src/index.ts), the name ledgers the removal reaches (e.g.one-authority-per-exported-name, any exported-name census), a retirement pin, pending changesets this removal falsifies (in the objectui#10533 form), and one.changeset/8024-…md('@object-ui/types'+'@object-ui/permissions':minorwith a BREAKING body; objectui never declaresmajor). Stop on breach and explain it in the reportContainer & model:
S,mode:subagent,model: opus—dispatch-gates --tier: 「no path-derived mandate … floor sonnet · default opus · ceiling fable」; executes ruling5809899168(UI closure batch 4 item 3, letter remove, maintainer 「同意」): first measure whether@objectstack/speccarries aPermissionGuardConfigshape, and stop and report if it doesClause-②: yes
Thread-read: 5809899168
Serial constraints cleared: open-PR file lists read 2026-09-25T10:54Z. No open PR and no live
pm:dispatchedclaim in the lane touchespackages/types/src/permissions.tsorpackages/permissions/src/index.ts.packages/types/src/index.tsis a shared barrel; this claim edits only itsPermissionGuardConfigline. Onorigin/mainthe name appears only atpermissions.ts:182and the two re-exports (index.ts:660,permissions/src/index.ts:42), in no doc and nowhere in*.md.Clause-②: yes— a published type export is removed from@object-ui/typesand@object-ui/permissions(ruling).Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
os-dev-report
{
"issue": 8024,
"status": "done",
"branch": "claude/issue-8024-retire-permission-guard-config",
"pr": "#10605",
"session": "session_014mXUNuFomfj24w7s1pZzhN — mode:subagent, so this is the parent seat session (PM claim 5831230763)",
"premise_still_valid": true,
"summary": "Retired
PermissionGuardConfigfrom @object-ui/types and @object-ui/permissions, following ruling 5809899168 (letter remove). At the old site inpackages/types/src/permissions.tsthe declaration is replaced by a//RETIRED note. The note namesPermissionGuardProps(PermissionGuard.tsx) as the shape the shipped guard reads, and saysfallback: 'redirect'/redirectPathNEVER EXISTED as a capability. There is no 'not yet supported' wording and no redirect or config-driven branch. Each barrel's re-export line is now a//pointer. Tombstone form: removed outright, following the objectui#7519 whole-type-name precedent (the retire-vs-remove discriminator onKanbanColumn.color/ChatbotSchemain complex.ts, as objectui#7678 amended it: a whole exported type name has no surviving carrier). The note cites the rule and does not restate it, so the 9684 agreement pin stays green. Also added: one retirement pin, and.changeset/8024-retire-permission-guard-config.md(types + permissions bothminor, BREAKING body). H0: the spec carries no such shape, so this was one leg with no re-scope. H3: no ledger moved. H5: no pending changeset names the type. The card's assignee was os-elon-musk on arrival; I did not write it. The PR's assignee is set to os-elon-musk.","tests": "HEAD efdc3e3; BASE 1daf477.\nH0 spec measurement:\n- Installed @objectstack/spec@17.4.0 (the lockfile pin), whole package: PermissionGuardConfig / PermissionGuard / redirectPath / fallbackComponent each files=0. Lit control: PermissionSetSchema files=46.\n- The only
fallbackz.enum in src isfallback: z.enum(['404', 'proxy', 'custom'])in src/api/dispatcher.zod.ts (HTTP dispatcher routing, not a guard). No 'hide','disable' enum exists.\n- objectstack origin/main a08e059 packages/spec:git grepexit=1 for all four names. Control PermissionSetSchema exit=0, 24 files. The objectstack whole tree is also exit=1 for all four.\nH1 on BASE, whole objectui tree:\n- PermissionGuardConfig: 3 hits (the declaration and 2 re-exports). redirectPath: 1. fallbackComponent: 1. Control PermissionGuardProps: 3.\n- Full-history pickaxe after unshallow: -S"'redirect'" over packages/permissions finds 0 commits. -S redirectPath over packages finds only 954168a, which added the declaration. Control -S fallbackContent finds 49ddc8b.\nPin RED on base (the 3 source files reverted to BASE under a trap; pin kept):\n- Checked on disk: decl=1 typesReexport=1 permReexport=1 note=0.\n- vitest pin: 'Tests 3 failed | 3 passed (6)'. The failures are both barrel legs and the note leg.\n- tsc -p packages/types/tsconfig.test.json: 'permission-guard-config-retired-8024.test.ts(60,5): error TS2578: Unused @ts-expect-error directive.' and the same at (62,5).\n- Restore:git checkout HEAD --on the 3 paths.git diff HEADis empty, status is clean, and hash-object equals HEAD blob: MATCH x3.\nPin GREEN on head:\n-vitest run --maxWorkers=2 packages/types/ packages/permissions/ scripts/__tests__/one-authority-per-exported-name-6273.test.ts: 'Test Files 247 passed (247)' / 'Tests 5363 passed (5363)', VERDICT command-exit 0.\n- Verbose run of the pin, tombstone-discriminator-agreement-9684 and one-authority-6273: 'Test Files 3 passed (3)', 'Tests 39 passed (39)'. Pin 6/6.\n-tsc -p tsconfig.test.json --listFilesincludes the pin, with 0 errors.\nNo ablation: the pin reads source and resolves../index.jsrelatively, so no dist path is involved.","gates": [
"build
pnpm --workspace-concurrency=2 --filter '@object-ui/permissions...' build: VERDICT command-exit 0. Dist completeness ✓ for types (130 files) and permissions (30). The built .d.ts carries the name 0 times. PermissionGuardProps is still in permissions dist (2 files).","type-check
pnpm --filter @object-ui/types --filter @object-ui/permissions run type-check: VERDICT command-exit 0. The script names were echoed (types: tsc --noEmit, examples and test projects; permissions: tsc --noEmit and test project).","lint
pnpm --filter @object-ui/types --filter @object-ui/permissions run lint: VERDICT command-exit 0. 0 errors. Warnings are no-explicit-any only (298 types, 27 permissions), none in a touched file.","node scripts/check-changeset-presence.mjs: exit 0. '✅ 4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'.",
"pnpm check:new-line-citations: exit 0. 'VERDICT new-cross-file-line-citations: 0 new citation(s)'.",
"pnpm check:control-bytes: exit 0. '✅ check-control-bytes: OK (scanned 8690 tracked text file(s); skipped 85 binary)'.",
"check-changeset-no-major / -fixed / -overwrite: all exit 0.",
"pnpm check:pending-changeset-literals: exit 0. '✅ No test source names a pending changeset.'",
"pnpm check:changeset-claims (report-only): exit 0. One pending changeset names packages/types/src/index.ts: the GanttConfig paragraph. I re-read it and it is still true.",
"check-type-check-coverage: exit 0. '43/43 packages compile their tests'.",
"pnpm check:test-path-roots: exit 0. OK.",
"check-governed-queue-guard --test over the 5 paths: 'NOT GOVERNED'.",
"check:readme-exports: DECLARED NARROWING. Only types and permissions were built. The --list rows for both READMEs are 36 real + 1 local-declaration, 0 fabricated. The full run exits 1 on the collapse floor because 34 packages are unbuilt; those other packages are NOT MEASURED. Their export surfaces cannot move with this diff: star re-exports of @object-ui/types or @object-ui/permissions exit=1 (0 hits), while the control star pattern has 324 hits. CI builds ./packages/* first."
],
"files_changed": [
"packages/types/src/permissions.ts",
"packages/types/src/index.ts",
"packages/permissions/src/index.ts",
"packages/types/src/tests/permission-guard-config-retired-8024.test.ts",
".changeset/8024-retire-permission-guard-config.md"
],
"deviations": [
"The worktree did not exist when dispatched. I created it detached at the shared origin/main, ran fetch inside it (origin/main moved to 1daf477), and branched from there. No edit or fetch touched the shared objectui checkout.",
"I unshallowed the objectui object store (
git fetch --unshallow origin main, run inside the worktree) so the pickaxe could back 'never existed' over the full history. The object store is shared, so every objectui worktree is now full-depth.","To read the role file I ran
git fetch origin mainin the shared objectstack checkout, as the order directs (reading at origin/main). The shared objectstack checkout scripts/pm matches origin/main, so the write tools I ran from it are the current ones.","Commit trailers are model-free (
Co-Authored-By: Claude), because the role file overrides the harness's model-named trailer. The PR body ends in the role file's session-URL footer.","check:readme-exports was narrowed; the narrowing is declared under gates.",
"Deleting the worktree and its node_modules succeeded without --force."
],
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "3 REST writes, all through scripts/pm and the fleet-write relay (each relay act is a POST /repos/objectstack-ai/objectstack/dispatches that the relay run executes): (1) pr_create POST /repos/objectstack-ai/objectui/pulls, draft (PR 10605); (2) label-write --assign os-elon-musk, POST /repos//issues/10605/assignees (read back: MATCHES); (3) this os-dev-report comment, POST /repos//issues/8024/comments via post-stamped. Also 2 git pushes (the empty-branch probe and efdc3e3), which are not REST.",
"open_questions": [],
"out_of_scope_findings": [
"none filed. Observations recorded only as PR Acceptance notes, all outside classes a/b/c: PermissionGuard has no production mount (README examples only); its disable arm uses an inline style; the permissions.ts module header still says the types align with spec v2.0.1."
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
✅ ACCEPT: objectui#10605 at
efdc3e3; ready + auto-merge follow this ACCEPTdomain:uiseat #2,session_014mXUNuFomfj24w7s1pZzhN. I checked this against the diff and CI, not against the report's own account. Claim5831230763. It executes ruling5809899168(UI closure batch 4 item 3, letter remove).PermissionGuardConfig/PermissionGuard/redirectPath/fallbackComponentin@objectstack/spec@17.4.0(the lockfile pin), at objectstackorigin/main, and in spec2.0.1(the version the old header claims alignment with); lit controlPermissionSetSchema. So there is one leg and no re-scopetypes/dist/permissions.d.ts,types/dist/index.d.tsandpermissions/dist/index.d.ts. It is a real removal from two published surfaces//RETIRED note at the old site namingPermissionGuardProps. This is the discriminator incomplex.ts("a whole exported type name has no carrier and is removed outright"), and the same form objectui#7519 used (mobile.ts). Keeping aneveralias exported would leave the name on the published surface, against the ruling's first clause. The published pointer toPermissionGuardPropsis the changeset, which ships asCHANGELOG.mdin both tarballsfallback: 'redirect'/redirectPathNEVER EXISTED as a capability" is true over full history: pickaxe'redirect'inpackages/permissions→ 0 commits, andredirectPath→ only the declaration's birth. There is no "not yet supported" and no redirect branchTS2578× 2, since the@ts-expect-errorimport probes fire for "no exported member"), green on head, with thePermissionGuardPropscontrolone-authority-6273,tombstone-discriminator-agreement-9684and the README export check are all greenClause-②: yes/ semvertypesminor+permissionsminor, with a BREAKING body (AGENTS.md §9: nevermajor). The upgrade section names what fails and what to use. The title's!matchesCONTRACT_REVIEW_TIERon this head, record5831695886. The seat read the reviewer transcript: every turn ran at tierefdc3e3: 40 success, 3 skipped by design, 0 red.git merge-treeagainst main is cleanFixes #8024onlyFindings, each with its disposition
@object-ui/permissionsimport givesTS2724 … Did you mean 'PermissionGuard'?, naming the component, notPermissionGuardProps, and the//note does not reach the typings. The shipped CHANGELOG is the pointer, and that is judged sufficient and consistent with precedent ⇒ Acceptance notes.permissions.tsmodule header still says "aligned with @objectstack/spec v2.0.1". That predates this PR and is stale (spec 2.0.1 never carried these types) ⇒ recorded, for the next edit to that file.PermissionGuardhas no production mount (README examples only), and its disable arm uses an inline style ⇒ recorded. Nothing is filed: there is no class a/b/c.Deviations recorded
git fetch --unshallow) to back "never existed" over full history. It is harmless, and every worktree is now full-depth.os-elon-musk), because this dev'slabel-writewent through.State
In one stroke:
readyand auto-merge (squash), plus thepm:dispatchedstrip on objectui#8932 (merged58b7b3dc), through the relay opspr_ready+automerge_enable+labels_remove.Generated by Claude Code
feat(types,permissions)!: retire PermissionGuardConfig from both publ…