Skip to content

feat(spec): HookContext.session 补声明 positions / preserveAudit(#5605) - #5722

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5605-session-positions-declare
Aug 6, 2026
Merged

feat(spec): HookContext.session 补声明 positions / preserveAudit(#5605)#5722
os-zhuang merged 1 commit into
mainfrom
claude/issue-5605-session-positions-declare

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes#5605

按维护者 2026-08-06 的裁定 A:两个键都补进 HookContextSchema.session,并用 .describe() 把边界钉死。

这是 #5050 的镜像,不是它的另一半措辞

session.rolesdeclared-never-produced —— 声明了、没人产,所以 #5050 把它退役成墓碑。这两个键是 produced-never-declared —— 引擎在产、消费方在读、文档在教,契约里没有,所以补声明。同一个 session 块、方向相反、修法相反,这就是 #5605 单独立单的理由。

前提复核(对 origin/main 逐条核实,四条全部成立)

  • 生产方:packages/objectql/src/engine.tsbuildSession() —— :1494 写 positions: execCtx.positions,:1518 条件写 preserveAudit
  • 消费方:packages/objectql/src/plugin.ts:782const preserveAudit = session?.preserveAudit === true;(该处 session 形参是 any,所以类型层看不见这条读)。
  • 文档在教:content/docs/kernel/runtime-services/examples.mdx:37sharing-service.mdx:67,都是 positions: ctx.session?.positions
  • 契约没有:补之前 session 只有 userId / actor / organizationId / accessToken / isSystem / skipTriggers / skipAutomations + roles 墓碑。

⚠️ issue 指出该文件近期被 #5621 / #5668 连改两次 —— 已基于合并后的当前 origin/main 重新定位,行号与结构均以本分支基线为准。

先证红(两条通道,都是改动前在 origin/main 上实测)

parse 通道 —— HookContextSchema 刻意非 strict(文件头有说明:它是引擎交给 handler 的运行时形状,strict 会让引擎侧任何一次内部增强变成消费方的破坏性变更)。代价就是未声明的键被静默 strip:

输入 session: { userId: 'u1', positions: ['sales_manager'], preserveAudit: true }
parse 输出 : {"userId":"u1"}
has positions : false
has preserveAudit : false

生成的 reference 页恰恰以 HookContextSchema.parse(data) 作为消费示例 —— 照文档消费,调用方的任职信息就掉在地上。

tsc 通道 —— 按 content/docs/automation/index.mdx 的写法把 handler 标成 (ctx: HookContext):

error TS2339: Property 'positions' does not exist on type '{ userId?: string | undefined; ... }'.
error TS2339: Property 'preserveAudit' does not exist on type '{ userId?: string | undefined; ... }'.

两页 runtime-services 示例之所以看不出来,是因为它们把 ctx 标成了 any照文档抄 + 照文档标类型 = 编译失败

改了什么

1. 两个键的声明(packages/spec/src/data/hook.zod.ts),都放在 roles 墓碑上方 —— 墓碑注释自己写明的排序约束(见下文「渲染器陷阱」)。

  • positions: z.array(z.string()).optional()
  • preserveAudit: z.boolean().optional()

2. .describe() 按裁定措辞钉边界。 这不是装饰,是这单需要维护者拍板的全部原因:positions可读上下文,不是授权输入。hook 可以读它去描述调用方(转给 sharing service 当评估上下文、调整消息、打日志),但不得用它自己做访问判断 —— 权限由 security service 在 ExecutionContext 上裁决(能力授予 permissions、任职 positions、以及派生的 posture,ADR-0095 D3)。一个用这个数组重新决定访问的 hook,是在一个拿不到授权模型的地方重判一件已经判过的事 —— 结构上正是 roles 墓碑要防的那个错误,只是换了一代词汇。措辞纪律与 roles 退役处方一致,针对的是下一个作者(尤其 AI)。

preserveAudit 的 describe 写它真实的消费语义:#3493 的历史导入保留标记,server-set、opt-in、普通写不带,由内置审计 hook 读取,用来保留调用方提交的 updated_at / updated_by 而不是盖上导入时刻 —— 是一条盖戳策略,同样不是授权输入。

3. Pin(packages/spec/src/data/hook.test.ts,新增一个 describe 块),与 #5050 的墓碑块并列,两块互为镜像正好把这张契约表的两个漂移方向都钉住。

反向验证(方向先预测,再运行)

预测:删掉任一声明,两条通道都应转红。实测:

  • parse 通道:vitest run src/data/hook.test.ts4 failed | 68 passed(两条 preserve、buildSession() 形状、describe 边界)。
  • tsc 通道:check:test-typechecksrc/data/hook.test.ts: 9 type error(s) in a file the ledger does not cover。该文件不在test-typecheck-debt.json 里,所以这是硬门,不是被 ledger 兜住的软红。

一条诚实说明:新增的 5 条断言里,keeps both OPTIONAL 在反向验证下仍然绿。它断言的是「缺席」,而键被 strip 之后同样缺席 —— 它是伴随断言,不是 pin(少写 .optional() 会让它转红,这是它留下的理由)。测试注释里已如实写明「不要把它当作声明存在的证据」,以免下一个读者把一条因空而绿的断言读成覆盖。这里没有用 @ts-expect-error:本单要证的事实是「文档教的代码编译」,所以类型 pin 是一条正向标注读,回退时的失败形态是硬类型错误,而不是 TS2578 未使用指令。

生成物与渲染器陷阱(#5606)

roles 墓碑注释写明:生成的 reference 把内联对象渲染成前四个声明键加省略号,而 z.never() 没有 JSON-Schema type,会印成 any —— 所以墓碑必须待在形状底部,否则 references/data/hook.mdx 会开始宣传 roles?: any。新键因此加在墓碑上方,落在第 8/9 位。

结果:content/docs/references/data/hook.mdx 的 session 行一字未变,仍是 { userId?: string; actor?: string; organizationId?: string; accessToken?: string; … }。已两路确认 —— check:docs 绿(文件无需重新生成),并逐行目视核对生成页。

pnpm --filter @objectstack/spec check:generated:10/10 全绿(api-surface 读的是 built dist,必须先 build 才有效,已 build 后复跑)。

一处刻意未提交:跑生成器时 authorable-surface.base.json 被重锚到本分支的 merge base,带进了别人落的 api/Discovery:scoping 两个键 + baseRev。那不是本次改动的产物,已回退 —— 该 gate 复跑后自述「trails the merge base by 2 key(s) — expected right after a surface change lands」,是信息性提示,不红。删除类 ratchet 的锚点不该搭在无关 PR 里顺带前移。

验证

命令结果
pnpm --filter @objectstack/spec test317 passed (317) / 8088 passed (8088);合入 main 后复跑 8090 passed (8090)
pnpm --filter @objectstack/spec typecheck绿(tsc --noEmit + check:test-typecheck OK),合入后复跑仍绿
pnpm --filter @objectstack/spec check:generatedAll 10 generated artifacts are up to date,合入后复跑仍 10/10
pnpm --filter @objectstack/objectql typecheck绿
pnpm --filter @objectstack/objectql test121 passed (121) / 1970 passed (1970)
node scripts/check-nul-bytes.mjsOK(另做控制字符自扫,无命中)
远端 CI(本 PR head)24 项 check runs 全部 success/skipped,无 failure

objectql 一并跑了,因为它是生产方:buildSession() 的返回值现在被真正声明的类型覆盖(此前 as HookContext['session'] 把两个键遮了过去)。

影响面

纯增量:两个键都 optional,形状仍非 strict,现存 context / handler / 存量元数据都不受影响。HookContext 按操作构造、从不落库,没有任何东西需要迁移。changeset 记 @objectstack/spec minor。

一处如实说明:本地有两个未推上来的提交

本分支在本地还有两个提交没能推送 —— push 被拒,原因是本 PR 已被(非本 session 的动作)标记 ready 并加入 merge queue,排队中的分支不允许更新。两个提交都不含内容变更:

  1. 一个 origin/main 的合并提交(AGENTS.md §10 的合并后复验;merge queue 本身就是把 PR 作为「合并到当前 main 的结果」来构建的,所以这个合并对正确性是冗余的);
  2. 一处纯注释措辞订正 —— 上面「诚实说明」那段的行内注释原写作「四条兄弟断言」,准确说法是「五条兄弟里的四条,第五条是 tsc 通道的 pin,vitest 判不了它」。

即入队的 head 与我的最终状态在本 PR 涉及的文件上只差这 6 行注释。留此说明以免后来者对不上分支状态;若需要,订正可作为后续小 PR。

附带发现(未在本 PR 修)

#5720 —— 同两页文档还在教 ctx.services?.sharing?.canEdit(...),而 hook 上下文(引擎构造的 handler ctx 与 body-runner 的沙箱 ctx)都没有services 这个键(它是 action ctx 的词汇)。可选链短路成 undefined,if (!ok) throw 于是无条件抛出 —— 照抄这个示例的 hook 会拒掉该对象上的每一次写入。同一个 ctx: any 标注同时架空了那段 {/* os:check */} 门禁,也正是它掩盖了本单的 TS2339。修法取决于「hook 访问 kernel service 的受支持通道是什么」这个待定问题,故按 Prime Directive #10 单独立单,未在本 PR 顺手改文档。

…5605)
Two keys the engine produces, consumers read and the docs teach were missing
from HookContextSchema.session. Declared per the maintainer ruling (A) on
#5605 — the mirror of the #5050 `session.roles` retirement: that key was
declared-never-produced (removed), these two are produced-never-declared
(added).
Because the shape is deliberately non-strict, the omission was silent:
`HookContextSchema.parse(ctx)` — the call the generated reference documents —
stripped both keys, and a handler typed `(ctx: HookContext)` as the automation
docs teach hit TS2339 on `ctx.session?.positions`. The two runtime-services
pages that teach that exact read compile only because they annotate `ctx` as
`any`.
`positions` carries the ruling's boundary wording in its `.describe()`:
readable context for hooks, never an authorization input — privilege is judged
by the security service on the ExecutionContext (permissions / positions /
derived posture), never by testing this array in a hook. Same discipline as the
`roles` tombstone, which the new keys sit ABOVE so the generated reference's
four-key inline summary does not surface the tombstone as `roles?: any`.
`preserveAudit` documents its real consumer semantics: the #3493
historical-import flag read by the built-in audit hook to keep a
caller-supplied updated_at/updated_by instead of stamping the import instant.
Both optional and additive; contexts are built per operation and never stored,
so there is nothing to migrate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
@vercel

vercelBot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 6, 2026 2:07am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude