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 (the export 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
PermissionGuard a config-driven arm that really implements redirect, and reconcile permission / action and fallbackComponent / fallbackContent to 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.
⚠️ Only the two spellings that CONTRADICT are asserted above; whether @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.
Found while compiling
packages/permissions/README.mdfor objectui#5174 batch 20 (theUNGATED_DOCSburn-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/typesdeclaresPermissionGuardConfig(packages/types/src/permissions.ts), and@object-ui/permissionsre-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,PermissionGuardin@object-ui/permissions, does not accept it and does not agree with it:PermissionGuardConfig(@object-ui/types)PermissionGuardProps(@object-ui/permissions)permission: string | PermissionActionaction: PermissionActionobject?: string(optional)object: string(required)fallback?: 'hide' | 'disable' | 'redirect' | 'custom'fallback?: 'hide' | 'disable' | 'custom'fallbackComponent?: stringfallbackContent?: ReactNoderedirectPath?: stringSo an author who takes the published type at its word can declare
fallback: '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.Measured, on
origin/main4bb33d7a4Grep over
packages,apps,examples,scripts,content,docs,skills,e2e, excludingdist/: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:
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/spechas a say inPermissionGuardConfig'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-devseat for objectui#5174 batch 20, from sessionsession_01FhBNJcLRZLe8M87VcUgpKr; left unassigned for PM triage.