Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Four more client sites still read the retired user.roles — role-addressed approvals and requiredRoles gates degrade silently at protocol 17 #5424

Closed

Description

@os-support-ai

Found while implementing objectui#5389 (which fixes the fifth site, useIsWorkspaceAdmin). #5389 scoped itself to that one hook, so these four are filed separately rather than folded in — three of them are authorization/routing-adjacent and at least one needs a semantics decision, not a mechanical rename.

Claim

user.roles is not emitted by the protocol-17 session face. Framework ADR-0090 D3 renamed it to positions, with no deprecation window. Five objectui sites still read it; #5389 fixes one; these are the other four.

Evidence — measured, not inferred

Booted a real single-tenant server on published @objectstack/cli / @objectstack/plugin-auth 17.1.0 (objectstack dev --seed-admin --fresh, Tenancy: single) and read GET /api/v1/auth/get-session for a permission-set-derived platform admin:

{
  "role": "user",
  "positions": ["user", "platform_admin"],
  "isPlatformAdmin": true
}

Object.prototype.hasOwnProperty.call(user, 'roles') on that payload answers false. The published bundle agrees at the source: @objectstack/plugin-auth@17.1.0's customSession returns exactly

user: { ...user, positions, isPlatformAdmin: platformAdmin, ...authGate ? { authGate } : {} }

and no roles key appears anywhere in its emission path.

The four sites

  1. packages/auth/src/AuthGuard.tsx:58

    const userRoles = user.roles ?? (user.role ? [user.role] : []);

    user.roles is always absent now, so every requiredRoles gate falls through to the single stored scalar. On the deployment shape above that scalar is 'user', so requiredRoles={['platform_admin']} (or any business position name) denies a user who genuinely holds it.

  2. packages/app-shell/src/hooks/sharedUserFeeds.ts:490 — approverIdentities() builds role: + name identities from u?.roles ?? [] only. At protocol 17 that array is always empty, so no role-addressed identity is ever sent: approvals addressed to a position rather than to a person vanish from the bell badge, the Approvals tab and Home's To-do card. This is the site the framework's own customSession docblock names — "Consumers that match individual role names (e.g. the Console approvals inbox resolving role-addressed approvers) also read positions — business names such as manager survive only there." Arguably as severe as useIsWorkspaceAdmin still reads user.roles — a key the v17 session payload no longer emits — so platform-admin detection (Setup/Studio visibility) silently fails wherever there is no active-member row #5389 and equally silent.

  3. apps/console/src/services/approvalsApi.ts:375 — the same approver-identity derivation, but it also splits user.role, so it degrades rather than dying: it yields role:user and loses every business position name. Its own comment still describes the two shapes as "multi-role shape (some auth providers)" vs better-auth's scalar, which is no longer the distinction that matters.

  4. packages/app-shell/src/console/AppContent.tsx:860 — forwards roles: (user as any).roles into expressionUser. Lowest severity of the four: the same object already forwards isPlatformAdmin and positions correctly, so this key is vestigial rather than load-bearing. It should go with the rest so the shape stops advertising a field that is always undefined.

The type that hid all five

packages/auth/src/types.ts declares roles?: string[] on the client AuthUser — a hand-copied mirror key with an ?, so nothing ever failed to compile. #5389 tried to retire it in the same pass and could not: removing it breaks site 1 at compile time (measured — src/AuthGuard.tsx(59,60): error TS2339: Property 'includes' does not exist on type '{}', because AuthUser's index signature then types the read as unknown). So the key is not dead; it has one live reader, and retiring it is gated on deciding site 1. #5389 left the declaration in place with a corrected docblock naming this issue.

What is NOT claimed

  • No runtime reproduction of sites 1-4 specifically. The session payload above is measured and re-runnable; what each consumer then does with the empty array is read from source, not observed in a browser.
  • Site 1 is not a mechanical rename. requiredRoles is a consumer-supplied list with its own semantics: whether it should read positions, whether it keeps the user.role fallback, and how it relates to packages/permissions' separate roles surface (fed by /api/v1/me, a different endpoint that is out of this scope) are open questions. Whoever takes this should settle them rather than guess.
  • packages/permissions has its own unrelated roles: string[] on its own user shape. Not in scope here.

Refs

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue, type Bug, domain:ui. Not a fresh product decision: framework ADR-0090 D3 renamed roles → positions with no deprecation window, and the session face measurably no longer emits roles — consumers following the rename is the restoration of an existing ruling, so this dispatches. Severity anchor is site 2 (sharedUserFeeds.approverIdentities): role-addressed approvals silently vanish from the bell/Approvals/To-do — the framework's own docblock names this consumer. Site 1 (AuthGuard.requiredRoles) reads positions per the ADR; whether the scalar user.role fallback stays and how it relates to packages/permissions' separate surface are the dev's to settle in the report (stop-and-report if it turns genuinely contractual). Retiring the roles?: string[] mirror key in packages/auth/src/types.ts belongs in this card once site 1 is fixed (per #5389's note). packages/permissions stays out of scope as filed.


    Generated by Claude Code

  3. claude commented on Aug 21, 2026

    @claude
    Contributor

    PM · 验收(PR #5555 已开,待 CI)+ 两个待决问题的裁定 + 一条必须记下的生产者发现

    交付

    实现完成于断网期间,已推送:PR #5555(draft,Refs #5424,非 Fixes——站点 1 待裁)。分支 claude/issue-5424-user-roles-retired-readers,head 638b29283,3 提交 / 7 文件,与派发 fence 逐一对上。站点 1(AuthGuard.tsx)与 packages/auth/src/types.ts 未触碰,拆分裁定守住了。

    ⚠️ 提交署名做过一次修正:原三个提交的 author/committer 是 sales@objectstack.ai,GitHub 会标 Unverified,已 rebase 重置为 noreply@anthropic.com。内容零变化——git diff 64330ca49 HEAD 为空,只有身份变了,基底 eba3a6eaa 未动。旧 SHA 作废。

    两个待决问题,我的裁定

    Q1(buildExpressionUser 抽出后多一条 react-refresh/only-export-components 警告)—— 选 A,保持现状。 留在 fence 内、0 errors、CI 不受影响,而且把规则的建议留在可见处。⛔ 明确不选 C:一条 eslint-disable 注释等于宣称这个安排永久没问题,而那是我没有立场下的判断——你自己也是这么说的,同意。B(新建模块)是个可有可无的后续,维护者想要再说。

    Q2(站点 2 与站点 3 身份来源不对称)—— 选 A,不动。 该分歧先于本卡存在,B 和 C 都是超出重命名的行为变更:B 会让每个用户凭空多出 role:<标量> 身份、可能扩大生产环境的匹配集;C 会移除协议 17 仍在发的一个真实身份来源。没有 pending_approvers 实际authoring 方式的读数,两边都不能选。若徽章数与列表的分歧将来被报成 bug,单独立卡。

    你查出的那条生产者发现,我复核了,而且它比看起来重要

    packages/auth/src/AuthProvider.tsx  (预览模式)
      setUser({ id:'preview-user', email, name, role, roles: [role] });
                                                      ^^^^^^^^^^^^^ 仍在生产退役拼写,且无 positions
    

    你建议挂到站点 1 的决策卡而不是单独派发,理由成立:预览模式恰恰就是还在喂 AuthGuard.requiredRoles 的东西,两者是同一根链条的两端。

    我要补一句你没说满、但对裁决很关键的推论:它证伪了「roles 已无生产者、只剩读者」这个前提。如果站点 1 的裁决走「让 AuthGuard 改读 positions」,预览模式下的用户将既没有 positions、又不再被读 roles,于是 requiredRoles 会对每个预览用户全部拒绝。修好主路径会打坏预览路径。这条必须进入裁决考量。

    ⚠️ 本条本应记在站点 1 的决策卡上,但那张卡此刻对本会话不可见(账号切换后有一段 issue 区间读不到)。先记在这里,以免丢失;可见性恢复后需转记过去。

    第二条(String(role).trim() 对非字符串会产出字面量 role:undefined)属实且当前不可达,你判断不硬化是对的——超出本卡缺陷类别且会扩大 diff,而派发令要求的「缺失键」负控制已经覆盖了今天真实可达的那一面。


    Generated by Claude Code


    Generated by Claude Code

  4. os-sales commented on Aug 21, 2026

    @os-sales
    Collaborator

    PM · Landing recorded, and site 1 escalated

    PR #5555 MERGED 2026-08-21T11:05:26Z (head 638b29283, 7 files). Sites 2, 3 and 4 are closed. The prior seat session (session_014zHsbJoTkTZeJQ5DLbRXrE) reviewed and accepted this PR but went dark at 10:43Z before it could record the landing — this comment closes that half-state.

    Card leaves pm:dispatched. It does not return to the queue: the residual is an access-control decision, which is maintainer floor, not seat discretion.

    Residual, re-measured on origin/main this round (not relayed)

    packages/auth/src/AuthGuard.tsx:58:

    const userRoles = user.roles ?? (user.role ? [user.role] : []);
    

    Protocol 17 emits no roles key at all, so on every real deployment the left operand is undefined today and this gate already decides on the scalar user.role alone. That is worth stating plainly because it changes the severity from "a rename left one site behind" to "an access-control gate is answering on the wrong input right now":

    • a gate written requiredRoles={['finance_approver']} refuses everyone who holds finance_approver as a position — the only place protocol 17 puts it;
    • and it admits on the coarse scalar role, so role: 'admin' passes a gate meant for an admin position.

    Both directions are live. Under- and over-admission on the same line.

    packages/auth/src/types.ts:85 keeps roles?: string[] declared solely because this file compiles against it, and its own docblock names this card as the owner of the retirement.

    The constraint that decides it — verified, and it is why this cannot be a seat call

    packages/auth/src/AuthProvider.tsx:240-247, preview mode:

    id: 'preview-user',
    …
    roles: [role],        // no positions key anywhere in this object
    

    So preview mode is the one identity source still producing the retired key, and producing no positions. The obvious fix — point site 1 at positions — refuses every preview user at every requiredRoles gate, because the synthetic user has no positions to match. Marketplace demo, and every preview/demo surface built on it, goes dark.

    The prior session found this producer and correctly judged it load-bearing; I verified it rather than relaying it. It means there is no version of this change that touches only one file.


    四棱分析(业务角度)

    一句话问题: 权限门 requiredRoles 今天用错了输入来判断「你能不能进这个页面」——真实部署里它只看那个粗粒度的单一角色,看不见用户真正的岗位;而唯一还在喂旧字段的是演示模式,所以修好正式环境就会弄黑演示环境。

    选项(客户可感知的代价):

    做法 客户看到什么
    A 门改读 positions,同时把演示模式改成也发 positions 正式环境权限判断恢复正确;演示环境需同批改,漏改则演示全黑
    B 门同时读 positions 和 roles 两边都不黑,但等于把 ADR-0090 D3 明令禁止的别名复活,旧拼写永久留存
    C 保持现状 岗位持有者继续被错误拒绝,粗粒度角色继续被错误放行

    业务直译与行业类比: 相当于门禁系统改用工牌上的「部门」判断,但读卡器还在读已经停用的「职务」栏——正式员工刷不进自己该进的门,而临时演示用的假工牌反而畅通。

    四轴论证:

    • 实际业务需求 — 审批、财务等按岗位授权的页面今天就在错判,是已发生的客户可感知损伤,不是投机能力面。
    • 项目长远合理性 — A 收敛到唯一已发布拼写 positions,与 ADR-0090 D3 同向;B 是长期技术债,且违背该 ADR 的明文。
    • 防 AI 写错 — A 让「一个字段一个含义」成立,AI 写权限元数据时不会在两个拼写间猜;B 恰恰制造两个都对的拼写,是 AI 批量写错的温床。
    • 创业阶段不扩散 — A 是一次性收敛,不新增面;B 新增一条需要永久维护的兼容路径。

    推荐: A。回退: 若演示模式改造成本超出预期,可先只修正式路径并显式把演示模式标记为已知缺口立卡,⛔ 不走 B。

    本分析看不见什么: 我没有度量 requiredRoles 在真实客户部署里的实际调用点数量与它们各自门的是什么页面,所以「今天有多少客户正在被错误拒绝」这个量级未知;也没有读 packages/permissions 那条独立权限面与本门的关系(分诊评论点名过它,但本卡范围之外)。若这两点中任一显示影响面极小,C(保持现状、另行安排)可能反而是对的。


    Generated by Claude Code

  5. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    Maintainer ruling recorded (2026-08-22, supplemental to the decision-inbox digest, skills seat session_01ApyDuQY2fkunMCqXiqvBhR)

    This card was omitted from the 46-card digest and presented separately with a four-axis recommendation; the maintainer ruled: 同意 (Option A).

    Ruled: Option A — the gates read positions. AuthGuard's requiredRoles matches against user.positions (keeping the user.role scalar fallback), and all four sites are fixed in the same batch: the two approver-identity derivations (sharedUserFeeds.ts, approvalsApi.ts) derive role-addressed identities from positions, and the vestigial roles forwarding in AppContent.tsx is removed. Once the last reader is gone, the hand-copied roles?: string[] mirror key in packages/auth/src/types.ts retires with it, so any future misread dies at compile time. Explicitly NOT ruled: a compatibility shadow key re-emitting roles — that would undo the deliberate no-deprecation-window rename. The separate roles surface in packages/permissions stays out of scope, as the card states.

    State: needs-user-decision removed → pm:queue (ready for dispatch).


    Generated by Claude Code

  6. self-assigned this
    on Aug 23, 2026
  7. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 18
    Session: session_01EuPCi56cnGyykygi3z9w4m
    Branch: claude/issue-5424-authguard-positions-residual
    Worktree: objectui-issue-5424
    Domain: domain:ui
    File surface: packages/auth/src/AuthGuard.tsx, packages/auth/src/AuthProvider.tsx, packages/auth/src/types.ts and their tests (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: claude-fable-5
    Clause-②: yes
    Serial constraints cleared: packages/auth is free — #5619/PR #5718 merged 22:42Z and released it. Round-18 siblings fenced disjoint: #5344 (packages/app-shell/src/views/metadata-admin/inspectors/) and #5627 (apps/console/src/components/FormPage.tsx). Predecessor on this card, PR #5555, merged 2026-08-21T11:05Z (sites 2/3/4).

    Tier basis. No path-derived mandate (objectui has no scripts/pm/; the objectstack dispatch-gates.mjs is the wrong tree and is not used). Content judgement: this card changes the accept/reject behaviour of an access-control gate and retires a key from a published type. That is clause ② on its face, so it is dispatched at CONTRACT_REVIEW_TIER (claude-fable-5) rather than below it — under the maintainer's standing authorization for this seat to dispatch above its own served tier (2026-08-22: 「你虽然是 opus,但是可以派 fable 的卡」).

    ⚠️ Scope is the RESIDUAL, not the whole card. The maintainer's ruling (2026-08-22, 同意 / Option A) reads "all four sites are fixed in the same batch", but it was recorded after PR #5555 had already merged sites 2, 3 and 4 (2026-08-21T11:05Z). Do not re-fix them. What remains: site 1 (AuthGuard), the roles?: string[] mirror-key retirement in types.ts, and the preview-mode producer below.

    Premise re-verified on origin/main c7cd2b60b — every line below measured this round, not relayed:

    • AuthGuard.tsx:58 — const userRoles = user.roles ?? (user.role ? [user.role] : []); and :59 matches requiredRoles.some(...). Unchanged since filing.
    • AuthProvider.tsx:250 — preview mode still emits roles: [role], and no positions key in that object.
    • types.ts:85 — roles?: string[] still declared.

    The severity is worse than a stale rename, and both directions are live. Protocol 17 emits no roles at all, so today the left operand is always undefined and this gate already decides on the coarse scalar user.role alone: a requiredRoles={['finance_approver']} gate refuses everyone holding that as a position (the only place protocol 17 puts it), and admits on role: 'admin' for a gate meant for an admin position. Under- and over-admission on one line.

    ⚠️ The constraint that made this a maintainer call, and the thing to measure first. Preview mode is the one identity source still producing the retired key and producing no positions. The naive fix — point site 1 at positions only — refuses every preview user at every requiredRoles gate, and every demo surface built on it goes dark. My reading, which is a PM mechanism assumption and not a ruling: the ruling's "keeping the user.role scalar fallback" is exactly what prevents that, because preview mode does set role. Measure whether the scalar fallback fully covers preview mode, or whether preview must also emit positions. If it must, that is inside this card's fence (AuthProvider.tsx is granted) — say so and do it; do not leave preview half-fixed.

    ⛔ Explicitly NOT ruled and not to be invented: a compatibility shadow key re-emitting roles. That would undo the deliberate no-deprecation-window rename of ADR-0090 D3. ⛔ packages/permissions has its own unrelated roles surface and stays out of scope, as the card states.


    Generated by Claude Code

  8. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    os-dev-report

    {
      "issue": 5424,
      "status": "done",
      "branch": "claude/issue-5424-authguard-positions-residual",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5724",
      "premise_still_valid": true,
      "summary": "Residual implemented per the 2026-08-22 ruling (Option A) at head cc6e27d7a: `AuthGuard.requiredRoles` matches `user.positions` with the `user.role` scalar fallback kept only for sessions lacking the key entirely; preview mode emits `positions: [role]` instead of the retired `roles` key (it was the last producer); the `roles?: string[]` mirror key is removed from `types.ts` now that the reader count is zero (verified: only AuthGuard.tsx:58 read it repo-wide outside comments). Issue body verified NOT truncated - it ends with the `Refs` list naming objectui#5389, cloud#1442, framework ADR-0090 D3 / framework#6011 / PR#6048. Sites 2/3/4 verified already landed on origin/main c7cd2b60b (PR #5555); not re-fixed. Every Zone-2b premise re-measured and confirmed, including the prior TS2339 blocker, reproduced verbatim during the type-probe leg. No compatibility shadow key; `packages/permissions` untouched; fence respected (5 files + 1 changeset, all in packages/auth + .changeset).",
      "preview_mode_finding": "Measured, not assumed (reverse leg B: AuthProvider.tsx reverted alone under the new gate): preview users still passed every `requiredRoles` gate via the scalar fallback - the PM inference is CONFIRMED, the scalar fallback fully covers preview at AuthGuard gates. Preview emitting `positions: [role]` was still done in the same batch because the escalation's Option A table (which the maintainer approved) names it, it retires the last producer of the retired key, and it shapes preview identities like protocol-17 sessions for the OTHER `positions` consumers (approver identities, workspace-admin leg 3). The guest identity (`enabled={false}`) was measured too: never produced `roles`, scalar fallback keeps it behavior-identical, untouched.",
      "negative_pins": "authGuardPositions-5424.test.tsx: (1) coarse scalar REFUSED when `positions` present - catches the over-admission the old line had (red before fix); (2) neither position nor matching scalar -> still refused - catches any fix that widens admission; (3) present-but-EMPTY `positions` refused - catches scalar fallback re-opening over-admission on an authoritative server answer; (4) retired `roles` spelling refused - catches ADR-0090 D3 dialect resurrection (was ADMITTED before the fix; second copy of this pin in AuthProvider.test.tsx); (5) preview viewer refused at an admin gate - nobody new passes; (6) preview identity `hasOwnProperty('roles')` === false - catches the producer coming back.",
      "tests": "All at cc6e27d7a, committed BEFORE any reverse leg; vitest resolves workspace source via the root config (no dist in the loop, so no rebuild step applies - mutations were confirmed on disk by anchored grep counts instead, restores by grep + clean `git status`). (a) `pnpm exec vitest run packages/auth/` -> `Test Files 20 passed (20) / Tests 209 passed (209)`, verify-lock VERDICT command-exit 0. (b) Reverse A - AuthGuard.tsx reverted to origin/main (grep: old-read 1 / new-read 0): `4 failed | 5 passed`, and the four reds are exactly the direction pins (admits position-holder, refuses coarse scalar, empty-positions authoritative, retired spelling dead); restored. (c) Reverse B - AuthProvider.tsx reverted alone (grep: old-producer 1 / new-producer 0): `1 failed | 8 passed`, only the producer-retirement pin red; restored. (d) Type-probe: pasted `user?.roles?.includes('admin')` -> `@object-ui/auth` type-check FAILED with `src/AuthGuard.tsx(55,21): error TS2339: Property 'includes' does not exist on type '{}'` (the card's measured signature); restored, probe-count greps 1 -> 0.",
      "gates": "Derived from objectui's own workflows/root package.json (objectstack's dispatch-gates.mjs NOT used, per dispatch). All verdicts quoted from the gate's own output, exits captured before any pipe: vitest packages/auth PASS (above); `turbo run type-check --filter=...@object-ui/auth` - auth AND ITS DEPENDENTS, downstream consumer direction - `Tasks: 40 successful, 40 total`, verify-lock VERDICT command-exit 0; `turbo run lint --filter=@object-ui/auth` (+ root lint) `0 errors, 29 warnings` all pre-existing - declared narrowing: population is per-package eslint over `**/*.{ts,tsx}` from `eslint.config.js`, the 5 changed files re-linted via `--format json` (5 files, 0 errors, 4 warnings all pre-dating this diff), invariance holds because no type-aware linting (`projectService`/`parserOptions.project` absent) and no custom rule reads other files, so untouched packages' verdicts cannot move; CI runs the full farm. `check:control-bytes` exit 0 `OK (scanned 4778 tracked text file(s))`; `check:phantom-deps` exit 0 `Every in-scope import is declared`; `check:esm-specifiers` exit 0; `changeset:check` exit 0 `No changeset declares a major bump`; `check-changeset-presence` exit 0 (`.changeset/authguard-positions-5424.md`, minor). CI on PR #5724 not waited on, per standing contract - PM reads convergence.",
      "files_changed": [
        "packages/auth/src/AuthGuard.tsx",
        "packages/auth/src/AuthProvider.tsx",
        "packages/auth/src/types.ts",
        "packages/auth/src/__tests__/AuthProvider.test.tsx (fixture triage: roles-array fixture converted to positions + companion negative)",
        "packages/auth/src/__tests__/authGuardPositions-5424.test.tsx (new)",
        ".changeset/authguard-positions-5424.md (minor)"
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  9. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    ACCEPT (on merits) — PR #5724 · reviewer of record, domain:ui seat, round 18

    ⚠️ On merits, not yet cleared to land. Gates read by name at head cc6e27d7a, two minutes after the PR opened: 12 success + 3 skipped + 7 in_progress (four shards, Type Check, Lint, Doc Snippet Type Check) = 22. ⛔ Not flipped ready and not enqueued until 19 success + 3 skipped is verified by name.

    Fence verified against the tree: 6 files — AuthGuard.tsx, AuthProvider.tsx, types.ts, two test files, one changeset. Nothing outside packages/auth + .changeset. packages/permissions untouched, as the ruling requires. Sites 2/3/4 confirmed already landed on origin/main and not re-touched.

    Content spot-checked independently of the report: the gate now matches user.positions, falling back to the scalar "only when the session carries no positions key at all"; types.ts carries a tombstone where roles?: string[] was, and positions?: string[] is still declared at :377 — the control that makes the removal a real absence rather than a broken grep.

    The PM assumption was measured, not inherited — and the measurement design is the part worth keeping

    I flagged as a mechanism assumption that the ruling's "keeping the user.role scalar fallback" is what stops preview mode going dark, and asked for it to be measured rather than adopted. The dev isolated it properly with a targeted reverse leg: reverting AuthProvider.tsx alone, under the new gate, gave 1 failed | 8 passed — only the producer-retirement pin red, and both preview gate cases green. That is a clean single-variable measurement of exactly the claim, and it confirms the inference: preview users still pass every gate via the scalar path.

    Preview then emits positions: [role] anyway — correctly, because Option A names it, it retires the last producer of the dead key, and it aligns preview with every other positions consumer. Fixing it because the ruling says so, having first proven the sky would not have fallen otherwise, is the right order.

    A positive control I want to single out. Removing a key from a type is easy to do cosmetically — the declaration disappears and nothing proves anything downstream changed. The dev pasted user?.roles?.includes('admin') back in and got error TS2339: Property 'includes' does not exist on type '{}' — the card's own measured signature, from when the prior attempt to retire this key failed for exactly that reason. That converts "the key is gone" from an assertion into a demonstration that a future misread dies at compile time, which is the whole point of the retirement.

    Both directions pinned, as this being an access-control gate required. The four direction pins are named and each turned red under reverse leg A (AuthGuard.tsx reverted): position-holder admission, coarse-scalar refusal, empty-positions authority, and the retired spelling dead. That third one is a semantic refinement worth recording: an empty positions array is an authoritative "no positions", not a missing signal, so the scalar fallback is not consulted for it. A fix that only proved "the right people now get in" would have left over-admission untested; this one tests both.

    Type-check ran in the downstream-consumer direction — turbo type-check --filter=...@object-ui/auth, i.e. auth and its dependents, 40/40 tasks. That is the direction a contract change owes, and it is what a package-scoped run would have missed.

    Clause ② at-tier, no review chain owed. Declared yes and dispatched at claude-fable-5 = CONTRACT_REVIEW_TIER, under the maintainer's standing authorization; needs:contract-review does not exist in this repo and none is owed for an at-tier dispatch.

    Landing blocked only on the seven pending gates. Closes via Fixes #5424 on merge.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions