feat(spec): HookContext.session 补声明 positions / preserveAudit(#5605) - #5722
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 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:
|
os-zhuang
marked this pull request as ready for review
August 6, 2026 02:29
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5605
按维护者 2026-08-06 的裁定 A:两个键都补进
HookContextSchema.session,并用.describe()把边界钉死。这是 #5050 的镜像,不是它的另一半措辞
session.roles是 declared-never-produced —— 声明了、没人产,所以 #5050 把它退役成墓碑。这两个键是 produced-never-declared —— 引擎在产、消费方在读、文档在教,契约里没有,所以补声明。同一个session块、方向相反、修法相反,这就是 #5605 单独立单的理由。前提复核(对
origin/main逐条核实,四条全部成立)packages/objectql/src/engine.ts的buildSession()—— :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:37与sharing-service.mdx:67,都是positions: ctx.session?.positions。session只有userId/actor/organizationId/accessToken/isSystem/skipTriggers/skipAutomations+roles墓碑。origin/main重新定位,行号与结构均以本分支基线为准。先证红(两条通道,都是改动前在
origin/main上实测)parse 通道 ——
HookContextSchema刻意非 strict(文件头有说明:它是引擎交给 handler 的运行时形状,strict 会让引擎侧任何一次内部增强变成消费方的破坏性变更)。代价就是未声明的键被静默 strip:生成的 reference 页恰恰以
HookContextSchema.parse(data)作为消费示例 —— 照文档消费,调用方的任职信息就掉在地上。tsc 通道 —— 按
content/docs/automation/index.mdx的写法把 handler 标成(ctx: HookContext):两页 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 的墓碑块并列,两块互为镜像正好把这张契约表的两个漂移方向都钉住。反向验证(方向先预测,再运行)
预测:删掉任一声明,两条通道都应转红。实测:
vitest run src/data/hook.test.ts→4 failed | 68 passed(两条 preserve、buildSession()形状、describe 边界)。check:test-typecheck→src/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-Schematype,会印成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 typechecktsc --noEmit+check:test-typecheckOK),合入后复跑仍绿pnpm --filter @objectstack/spec check:generatedAll 10 generated artifacts are up to date,合入后复跑仍 10/10pnpm --filter @objectstack/objectql typecheckpnpm --filter @objectstack/objectql test121 passed (121)/1970 passed (1970)node scripts/check-nul-bytes.mjsobjectql 一并跑了,因为它是生产方:
buildSession()的返回值现在被真正声明的类型覆盖(此前as HookContext['session']把两个键遮了过去)。影响面
纯增量:两个键都 optional,形状仍非 strict,现存 context / handler / 存量元数据都不受影响。HookContext 按操作构造、从不落库,没有任何东西需要迁移。changeset 记
@objectstack/specminor。一处如实说明:本地有两个未推上来的提交
本分支在本地还有两个提交没能推送 —— push 被拒,原因是本 PR 已被(非本 session 的动作)标记 ready 并加入 merge queue,排队中的分支不允许更新。两个提交都不含内容变更:
origin/main的合并提交(AGENTS.md §10 的合并后复验;merge queue 本身就是把 PR 作为「合并到当前 main 的结果」来构建的,所以这个合并对正确性是冗余的);即入队的 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 顺手改文档。