refactor(spec)!: 退役 HookContext.session.roles —— 声明过、被两条死分支读过、从未被生产 (#5050) - #5621
Conversation
… two dead branches, never produced (#5050) `session.roles` on the runtime hook context had neither end: declared in `data/hook.zod.ts`, read only by the two plugin-approvals admin exemptions deleted in #4839 (PR #5049), and never written by `buildSession()` or anything else feeding a HookContext. ADR-0049 enforce-or-remove disposition: REMOVE. - tombstoned with `retiredKey()` (HookContextSchema is deliberately not `.strict()`, so a plain delete would strip the key silently — #3733/ADR-0104) - placed BELOW the live keys: the reference generator renders a `z.never()` as `any` inside an inline shape summary, so in its original 4th position it made `references/data/hook.mdx` advertise `roles?: any` (renderer gap filed #5606) - ADR-0087: a SemanticMigration (`hook-context-session-roles-retired`), NOT a D2 conversion — a HookContext is built per operation and never stored, so no source exists to rewrite (the `openApi31` / `activationEvents` shape) - pins both channels: the parse prescription and two `@ts-expect-error` directives, live since #5286/#5478 put the test layer in front of tsc - skills/objectstack-data hook reference no longer teaches the dead key Cross-repo consumer check ran in both directions (cloud/objectui, #4895's discipline). The action body's `ctx.session` is a different, untyped object that does carry `roles` — named explicitly here and filed as #5613 so it is not mistaken for a producer of this key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
PM 预记( 「ADR-0087 处置:语义迁移而非 conversion」一条 PM 初审认可(否决窗口开放):HookContext 是运行时现建、从不落库,conversion 无源可改, changeset 为 major(退役),与 v17 pre 窗口内既有退役单(如 #5293)同待遇, Generated by Claude Code |
Fixes #5050
前提复核(动手前对
origin/main实测)packages/spec/src/data/hook.zod.ts:374roles: z.array(z.string()).optional()session.roles命中只剩 #4839 留下的注释/pin/changeset,以及 spec 自己的夹具与技能文档buildSession()(packages/objectql/src/engine.ts:1479)逐字段构造 ——userId/organizationId/positions/accessToken/isSystem/actor/ skip 标记 —— 没有roles写入点跨仓按 #4895 的双方向做(#4865 的教训:退役前必须有阳性对照):
session.roles零命中;同一轮反查阳性 —— 它的 hook 消费方确实在读hookContext?.session?.userId(service-cloud/src/marketplace-visibility-plugin.ts:98、control-plane-org-scope-plugin.ts:237-248)。即「grep 能找到东西,只是找不到这个键」。roles在该仓存在,但都是/auth/me的 user 载荷 ——app-shell/src/layout/AppHeader.tsx:457等,另一张面,不受影响)。结论:前提成立。
退役路线
HookContextSchema刻意不是.strict()(文件头注释写明理由:引擎给上下文加字段——如 #3712 的provenance——不应变成消费方的破坏性变更)。所以按 playbook §2:HookContextSchema是公开导出,生成的 reference 页正文就以HookContextSchema.parse(data)作示例,「拿到上下文的人去 parse 一把」是被设计支持的用法retiredKey()墓碑 —— tsc 通道(输入类型never)+ parse 通道(报处方本身)ADR-0087 处置:语义迁移,不是 conversion(重点,请审这一条)
没有做 D2 conversion,是有意的:HookContext 是引擎每次操作现建的运行时上下文,从不落库 —— 没有任何
sys_metadata行、example 或 template 能携带这个键,os migrate meta无源可改。造一个没有对象的 conversion 只会让升级指南宣传一层不存在的覆盖。按仓内既有判例走 SemanticMigration(
MIGRATIONS_BY_MAJOR[17].semantic[]),与openApi31(#4579)、activationEvents(#4657)、workflow服务槽(#4451)同形:hook-context-session-roles-retired,surface: data.hookContext.session.rolesspec-changes.json、生成的升级指南、spec_changesMCP 工具os migrate meta那句(playbook 约定:只有 conversion 真的改源码时才写)闸门这边也自洽:
build-schemas.ts的 (b) 闸只走顶层键,session是内联嵌套对象(快照里只有data/HookContext:session一行,没有session.roles),所以它既不要求也不阻拦 —— 判据与闸门给出同一个答案。四张 ratchet 零变化 —— 这是正常读数
按 playbook「先定路线再决定该期待什么读数」:本次是内联嵌套键收窄,def 还在、导出面不变,所以
api-surface/authorable-surface/api-surface-signatures/json-schema.manifest全部字节不变(与 #4391 枚举值收窄同类,而非 #4834 整 def 删除)。check:authorable-surface前后皆绿即为此。authorable-surface.base.json的变动是gen:schema的机械重锚(baseRev指向本分支的 merge base),同 a9f32df / cdfbee2 等 spec PR 的既有行为。台账
packages/spec/liveness/hook.json治理的是 HookSchema(可授权元数据类型),HookContextSchema不在 walk 里,session.roles从来没有台账行 —— 所以既没有要留的墓碑行,也没有要删的孤儿行。已核对,无改动。一处真实的坑:文档把墓碑宣传成
any墓碑一开始留在原位(
session的第 4 个键),重生成后content/docs/references/data/hook.mdx变成:session | { userId?: string; actor?: string; organizationId?: string; roles?: any; … }z.never()没有 JSON-Schematype,渲染器落到prop.type || 'any';而内联 shape 摘要只印前 4 个键、放不下[REMOVED]处方。退役反而把这个键宣传成"随便写"的自由槽,正是 ADR-0033 陷阱对着文档的一面。本 PR 的处理:把墓碑挪到 shape 底部,摘要因此只展示 4 个活键(
userId/actor/organizationId/accessToken),并在源码注释里写明为什么,防止后人"整理"回去。两个真实通道(tsc + parse)完全不受影响。references/ui/theme.mdx:130({ base?: string; heading?: any; mono?: any },两个 #5021 的墓碑嵌套两层、整页没有任何一处出现它们的处方,描述列还是空的)。修它要全仓重生成 references,不该搭在本 PR 上。消费半径扫描的收获(#5046 的教训:按规则被谁消费扫,不是按改了哪个包扫)
扫 HookContext 的全部导入方时发现
packages/runtime/src/action-execution.ts:694的buildActionSession()确实写了roles: ec.positions,而且它的注释自称 "mirroring the hookctx.sessionshape"。这不证伪本次退役:那是 action body 的
ctx.session,另一个对象,裸any,不经任何 schema,永远不会变成 HookContext(沙箱侧ScriptContext.session?: unknown)。但它确实会让后来者拿着「我在 action 里明明读到了session.roles」来推翻这里的零生产方结论 —— 这正是 #4865 的形状。所以:reason、changeset 里全部显式点名这个邻居,并把口径从「repo 内没有生产方」收窄为「没有任何东西喂给 HookContext」ctx.session仍在生产roles(值是ec.positions)—— 自称「mirroring hook ctx.session」,而 hook 侧该键已按 ADR-0049 退役 #5613(它把 ADR-0090 D3 禁掉的roles拼法从positions又翻译回来了,且 action ctx 完全没有 schema)—— 涉及公开契约 + 安全词汇,登记不实现测试与反向自证
pin 测试 5 条(
packages/spec/src/data/hook.test.ts),方向在跑之前就先定好:还原roles: z.array(z.string()).optional()应当让 parse 断言转红、并让两条@ts-expect-error报 TS2578(#5478 之后 spec 测试层真的进 tsc,类型 pin 是活的)。实测:还原后
vitest run src/data/hook.test.ts:还原后
tsc --noEmit --project tsconfig.test.json:恢复墓碑后两者皆绿。预测方向 = 实测方向。
其中一条 pin 值得单独说:「墓碑 ≠ 变 strict」 —— 断言一个未知键仍然被静默 strip,只有退役键才响。这条是把「为什么不能直接删」和「为什么不能顺手加
.strict()」两个判断一起钉住。正式验证(worktree 内,统一走
flock /tmp/os-heavy-verify.lock):改了什么
packages/spec/src/data/hook.zod.ts—— 墓碑 + 就地注释(含"墓碑放底部"的理由与 [runtime] action body 的ctx.session仍在生产roles(值是ec.positions)—— 自称「mirroring hook ctx.session」,而 hook 侧该键已按 ADR-0049 退役 #5613 邻居声明)packages/spec/src/migrations/registry.ts—— 语义迁移条目 + step17 rationale 段落packages/spec/src/data/hook.test.ts—— 2 处夹具重判 + 5 条 pinskills/objectstack-data/references/data-hooks.md—— 3 处:字段表、类型清单、以及那个用死键做脱敏判断的示例(原写法isAdmin恒为undefined,改为按isSystem豁免,并写明按角色豁免应当落在字段级权限)packages/plugins/plugin-approvals/src/admin-exemption-retired.test.ts—— 仅注释:原文说"spec 现在声明roles",退役后改为过去式(该文件的 pin 与仍然拼roles: ['admin']的夹具故意保留,它们证明这个拼法在运行时同样什么都不授予)spec-changes.json、docs/protocol-upgrade-guide.md、content/docs/references/data/hook.mdx、authorable-surface.base.json@objectstack/specmajor顺带登记的三个 issue(Prime Directive #10,均未实现、未指派)
positions/preserveAudit—— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605 ——HookContext.session少声明positions/preserveAudit:引擎在生产、objectql/src/plugin.ts:782在读、两页 kernel 文档在教,契约里没有。本次的镜像方向(那两个是 produced-never-declared),补声明涉及安全语义,需裁定retiredKey()墓碑渲染成any—— 嵌套两层时连[REMOVED]处方都没有,退役键读起来像自由槽 #5606 —— 生成的 reference 把墓碑渲染成any(上文那个坑的根因,当下已在伤 theme.mdx)ctx.session仍在生产roles(值是ec.positions)—— 自称「mirroring hook ctx.session」,而 hook 侧该键已按 ADR-0049 退役 #5613 —— action body 的ctx.session仍在生产roles(值取自positions)🤖 Generated with Claude Code
https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
Generated by Claude Code