Repository navigation
[spec] HookContext.session 少声明了 positions / preserveAudit —— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605
Description
背景
#5050 退役的是「声明了、没人生产」的 session.roles。在核查它的生产方时,发现同一个 session 块存在方向相反的漂移:有两个键是引擎真的在生产、消费方真的在读、文档真的在教,但 packages/spec/src/data/hook.zod.ts 的 HookContextSchema.session 里根本没有声明。
这不是同一个 bug 的另一半措辞 —— roles 是 declared-never-produced,这两个是 produced-never-declared,修法完全不同(前者删,后者补声明),所以单独立单。
证据(均对 origin/main 核实)
生产方 —— packages/objectql/src/engine.ts 的 buildSession():
positions: execCtx.positions(逐字段构造 session 时直接写入)...((execCtx as any).preserveAudit ? { preserveAudit: true } : {})(data import: a "historical" import can't preserve original timestamps / audit fields — updated_at is stamped now, readonly fields stripped on upsert (#3479 follow-up) #3493 的历史导入审计保留标记)
消费方:
packages/objectql/src/plugin.ts:782——const preserveAudit = session?.preserveAudit === true;(审计戳 hook 的核心分支;该处session形参标注为any,所以类型层看不见这条读)
文档在教(两页,都把 positions 当作喂给 sharing service 的正规写法):
content/docs/kernel/runtime-services/examples.mdx:37——positions: ctx.session?.positions,content/docs/kernel/runtime-services/sharing-service.mdx:67—— 同上
契约里没有 —— HookContextSchema.session 声明的键是 userId / actor / organizationId / accessToken / isSystem / skipTriggers / skipAutomations(#5050 后再加一个 roles 墓碑)。没有 positions,没有 preserveAudit。
为什么这是缺陷而不是无害省略
- 文档教的代码在类型层编译不过。 一个按
content/docs/automation/index.mdx的写法把 handler 标注成(ctx: HookContext)的作者,写ctx.session?.positions会吃 TS2339;上面两页示例之所以看不出来,是因为它们把ctx标成了any。照文档抄 + 照文档标类型 = 编译失败,这是作者第一次写 hook 就会撞上的。 positions恰恰是 ADR-0090 D3 之后的权限词汇本体。ExecutionContext已经把roles改名成positions(packages/spec/src/kernel/execution-context.zod.ts:118,注释原话 "Formerlyroles")。[spec] 退役 HookContext session.roles —— #4839 双删后零消费方零生产方(ADR-0049) #5050 把 hook 侧最后一处roles拼法退役之后,hook 能看到的唯一任职词汇就是这个未声明的positions—— 契约上等于说「hook 拿不到任职信息」,而运行时明明给了。- Zod 非 strict 掩盖了它。
HookContextSchema刻意不 strict(见文件头注释),所以positions在 parse 时被静默 strip。也就是说:谁真的去HookContextSchema.parse(ctx)一把,谁就把positions弄丢了 —— 而生成的 reference 页(content/docs/references/data/hook.mdx)恰恰以HookContextSchema.parse(data)作为示例。
需要裁定的点(不要直接猜)
补声明的语义要维护者定,两条路差别很大:
- A. 两个键都补进
session声明(positions: z.array(z.string()).optional()、preserveAudit: z.boolean().optional()),配.describe()写清「仅供 hook 读取,授权由 security service 裁决,这里不是授权输入」。契约与运行时对齐,文档示例可以正常标类型。风险:positions出现在 hook 上下文里,可能被下一个作者读成「可以拿它做授权判断」—— 需要用roles退役同款的措辞把边界钉死。 - B. 认为 hook 本就不该看到任职信息,那么该删的是
buildSession()的positions写入 + 两页文档的教法,而不是补声明(即对positions走 enforce-or-remove 的 remove 一侧)。但注意preserveAudit有真实消费方(plugin.ts:782),它只能走补声明。
我的倾向是 A,理由:preserveAudit 无论如何都得补(有活的消费方,删不掉),而 positions 有两页文档 + sharing service 的真实用法在背书,删它是砍掉一个在用的能力;A 同时消灭「文档教的代码编译不过」这个当下就会撞到的问题。但 positions 是否应当出现在 hook 契约上是安全语义判断,按 ADR-0095 D3 的口径应由维护者拍板,故不擅自实现。
与其他单的关系
- 姊妹单
HookContext.input的契约注释声明批量写携带input.ast,引擎从不设置它(AST 只在 opCtx 上);同一张表也未描述 #5038 后 after 事件的按行形状 #5273(HookContext.input的契约注释与引擎实际行为不符)—— 同一张契约表的另一处漂移,但键不同、修法不同,不是重复。 - 反方向的 [spec] 退役 HookContext session.roles —— #4839 双删后零消费方零生产方(ADR-0049) #5050(
session.roles退役)已在 PR 中,本单不阻塞它:roles的删除与这两个键的补声明互不影响。
Activity
os-zhuang commentedon Aug 5, 2026
分诊:
needs-user-decision+domain:spec(预挂)。packages/spec/src/data/hook.zod.ts的HookContextSchema.session→domain:spec(A 选项即补声明于此;B 选项才会外溢到packages/objectql/src/engine.ts的buildSession()与两页文档)。positions/preserveAudit)与 B(对positions走 remove 一侧)的分叉在「hook 是否应见任职信息」这一安全语义,按 ADR-0095 D3 归维护者;preserveAudit无论如何只能补声明(有活消费方plugin.ts:782),真正待拍的是positions。session.roles退役,declared-never-produced 反方向,在飞)不重复且互不阻塞;HookContext.input的契约注释声明批量写携带input.ast,引擎从不设置它(AST 只在 opCtx 上);同一张表也未描述 #5038 后 after 事件的按行形状 #5273 是同一契约表的另一键,修法不同,不收敛。packages/spec恒归 spec 座位,拍板后由 spec 车道派发。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
维护者裁定(2026-08-06,经 spec 车道 PM
session_018fxLGQdatPbBUvCgiVxg6D誊写):A —— 两键补声明。positions: z.array(z.string()).optional()+preserveAudit: z.boolean().optional()进HookContextSchema.session,.describe()按 issue 预案钉死边界:「仅供 hook 读取,授权由 security service 裁决,此处不是授权输入」——与roles退役墓碑同款措辞纪律,防止下一个作者(尤其 AI)把它读成授权判断依据。文档教的代码从此可正常标类型。摘needs-user-decision,即刻派发。Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
PM 认领(spec 车道 PM,
session_018fxLGQdatPbBUvCgiVxg6D,2026-08-06)claude/issue-5605-session-positions-declare;工作树:../objectstack-5605-session-declare(基于最新origin/main);域:domain:specpackages/spec/src/data/hook.zod.ts(session补声明positions/preserveAudit,.describe()按裁定钉边界措辞)+ 配套 pin + 生成物(references/data/hook.mdx如动走生成器);sections声明为 string[] 但所有页面授权对象形式,hideFields完全未声明 #5611、routes.mcp 是 REST /discovery 发出、objectui 真实消费、但 ApiRoutesSchema 从未声明的键(#4828 同族,低一层) #5679、[spec] 为 action body 的ctx.session立契约 schema(按现状声明)—— #5613 契约先行裁定的第一阶段 #5697Generated by Claude Code
feat(spec): declare HookContext.session.positions / .preserveAudit (o…