feat(spec): 为 action body 的 ctx.session 立契约 schema(按现状声明,#5697) - #5721
Merged
Conversation
Phase 1 of #5613's contract-first ruling: declare what `buildActionSession()` builds today, change nothing about what it builds. - `ActionSessionSchema` / `ActionSession` in `@objectstack/spec/ui` (`ui/action-params.zod.ts`, beside the action runtime-context declarations it already carries): `{ userId?, organizationId?, roles? }`, non-strict, matching `HookContextSchema`'s wire posture. - `roles` declared as built and marked DEPRECATED in `.describe()` + JSDoc: the value is `ExecutionContext.positions` under the spelling ADR-0090 D3 forbids; the rename is #5613 phase 2 and no `positions` key is minted now. - Type-only on the runtime side: `buildActionSession()` returns `ActionSession | undefined`, `ActionHandlerContext.session` is the inferred type. `ScriptContext.session` stays `unknown` (one seam, two body kinds) with the two shapes documented. - Consistency pin executing the real producer: `packages/runtime/src/action-session-shape-contract.test.ts`. 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 2 package(s): 115 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
os-zhuang
pushed a commit
that referenced
this pull request
Aug 6, 2026
…5679) CI 上两条不同签名的红,都是合并 origin/main 之后才出现的: 1. `@objectstack/client` DTS build — TS2741: `getRoute()` 的约定回退表 `Record<ApiRouteType, string>` 按设计对 `keyof ApiRoutes` **全键必填**, 所以给 ApiRoutesSchema 新增一个已声明键就必须给出它的约定路径。这是我 消费半径漏掉的包。补 `mcp: '/api/v1/mcp'` —— 不是猜的,正是两个 discovery 生产者实测发出的值,所以回退值与被发现值一致而非互相打架;而且该表本就 是 unscoped 约定表(每行都是 /api/v1/...),恰好符合 /mcp 裸挂载的事实。 全仓复扫 `keyof ApiRoutes` / ApiRoutes 型字面量,确认没有第三处:其余站点 都是 `Partial<ApiRoutes>` 或只填两个必填键,不受影响。 2. `check:authorable-surface` — 合并带进了 #5721 的 `ui/ActionSession` 源, 但 authorable-surface.json 未在合并树上重生成。跑 gen:schema 重出,该文件 现同时含本单的 `api/ApiRoutes:mcp` 与 main 的 ActionSession 三键。 `authorable-surface.base.json` 的 baseRev 重锚照 #5358 再次剔除 —— 那是 main 前移导致的机械重锚,不属本单。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…bjectstack-ai#5701) (objectstack-ai#5752) * feat(spec): declare $icontains (ASCII fold), pin the $contains family case-sensitive, retire $regex (objectstack-ai#5701) The contract half of objectstack-ai#4706's maintainer ruling (B). Declaration only: no live rejection, no driver code touched, every backend answers exactly what it answered before. - `$icontains` on StringOperatorSchema / FieldOperatorsSchema / Filter: contains, ignoring ASCII case (A-Z against a-z) and nothing else. The boundary is stated for authors — `café` does NOT match `CAFÉ` — because ASCII is the one fold all five backends can deliver: SQLite without ICU (so turso and sqlite-wasm too) folds ASCII only, and promising Unicode would repeat the defect this retires. - `$contains` / `$notContains` / `$startsWith` / `$endsWith` are declared CASE-SENSITIVE, superseding the recorded "Case sensitivity should be handled at backend level" (Prime Directive objectstack-ai#13 — the old sentence is quoted in place so the reversal is findable from the sentence a reader remembers). What that non-guarantee bought, measured: three different answers across five backends, two of them inside driver-memory alone (query path folds full Unicode, the reference matcher is case-sensitive). - `RETIRED_FILTER_OPERATORS`: pure data, no behaviour. `$regex` and `$options` with prescriptions naming `$icontains`. Nothing rejects them here — the five existing refusal sites are wired to it by objectstack-ai#5702, and objectstack-ai#5710 must flip the one live producer (plugin-auth's ObjectQL adapter, on the authentication path) first or sign-in breaks. - `FILTER_TEXT_CASES` (`data/filter-text-conformance.ts`): the shared standard for case folding, literal comparands and the `$regex` refusal, with the `expectRejection` discriminant `filter-logic-conformance.ts` deliberately never grew. A sibling table rather than rows in that one, whose charter excludes both axes and warns against red rows for work nobody is dispatched to do. Registered in check:driver-conformance with one MEASURED DEBT row per driver pointing at objectstack-ai#5702 — 25 covered / 5 DEBT / 0 exempt, main stays green. `$icontains` is deliberately NOT in `FILTER_OPERATORS` yet. That array is a runtime allowlist, not a word list: driver-memory's shape gate derives its ACCEPTED set from it while its matcher's `default:` arm breaks. Measured by adding it early and rebuilding — the gate stopped refusing and `match({name:'zzz'}, {name:{$icontains:'acme'}})` returned true, i.e. the predicate was silently dropped and every row matched. A dropped predicate widens rather than narrows, which on an RLS read scope is a permission bypass (objectstack-ai#3948). It joins the array in the PR that implements it; `filter-operator-vocabulary.test.ts` pins the difference at exactly {$icontains} so neither adding a second staged operator nor clearing this one can happen silently. Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D Co-authored-by: Claude <noreply@anthropic.com> * chore(spec): regenerate api-surface + authorable-surface after merging main The merge driver (`merge=os-regen`, AGENTS.md §11) deferred both artifacts rather than text-merging them, and `pre-commit` collects that debt. Regenerated from the MERGED source, so the union includes objectstack-ai#5721's ActionSession entries alongside this branch's $icontains / FILTER_TEXT additions. `authorable-surface.base.json` is deliberately NOT bumped: it is the deletion gate's anchor, main only moves it when a key is REMOVED, and this branch only adds. `check:authorable-surface` confirms it — 'trails the merge base by 5 key(s) — expected right after a surface change lands'. Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…ck-ai#5679) (objectstack-ai#5743) * feat(spec)!: declare routes.mcp on ApiRoutesSchema, extend discovery gate to routes keys (objectstack-ai#5679) `/discovery` advertises `routes.mcp`, objectui reads it, and `ApiRoutesSchema` never declared it — objectstack-ai#4828's defect one level down, with the opposite disposition: `endpoints` was retired for having no reader, `mcp` has two real ones and is in fact the only `routes.*` key anything in objectui reads. `ApiRoutesSchema` is a plain z.object, which strips unknown keys, so any consumer parsing /discovery through the spec dropped `routes.mcp` silently. - spec: declare `mcp: z.string().optional()` as MEASURED off both producers — a path string, always the unscoped base (/mcp is mounted bare), optional rather than nullable (the key is absent, never null, when MCP is off or unserveable). - rest: drop the two `as any` casts at the emit site. Type-only; the emitted body is byte-identical. With the key undeclared, removing them produced two TS2339; with it declared, tsc returns to its ratcheted baseline of 2. - gates: the objectstack-ai#4828 conformance tests now check `routes` keys as well as top-level ones in all three producer packages, deriving the allowance from ApiRoutesSchema. Extended one level, not recursed. Corrects the issue's premise on one point: the runtime dispatcher's getDiscoveryInfo() also emits `routes.mcp` (its routes literal always carries the key, holding the path or undefined), so both producers were affected and the new gate went red on both before the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D * fix(client,spec): 补 client 路由约定表的 mcp 项,并在合并树上重生成 authorable-surface (objectstack-ai#5679) CI 上两条不同签名的红,都是合并 origin/main 之后才出现的: 1. `@objectstack/client` DTS build — TS2741: `getRoute()` 的约定回退表 `Record<ApiRouteType, string>` 按设计对 `keyof ApiRoutes` **全键必填**, 所以给 ApiRoutesSchema 新增一个已声明键就必须给出它的约定路径。这是我 消费半径漏掉的包。补 `mcp: '/api/v1/mcp'` —— 不是猜的,正是两个 discovery 生产者实测发出的值,所以回退值与被发现值一致而非互相打架;而且该表本就 是 unscoped 约定表(每行都是 /api/v1/...),恰好符合 /mcp 裸挂载的事实。 全仓复扫 `keyof ApiRoutes` / ApiRoutes 型字面量,确认没有第三处:其余站点 都是 `Partial<ApiRoutes>` 或只填两个必填键,不受影响。 2. `check:authorable-surface` — 合并带进了 objectstack-ai#5721 的 `ui/ActionSession` 源, 但 authorable-surface.json 未在合并树上重生成。跑 gen:schema 重出,该文件 现同时含本单的 `api/ApiRoutes:mcp` 与 main 的 ActionSession 三键。 `authorable-surface.base.json` 的 baseRev 重锚照 objectstack-ai#5358 再次剔除 —— 那是 main 前移导致的机械重锚,不属本单。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D --------- Co-authored-by: Claude <noreply@anthropic.com>
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 #5697
#5613 契约先行裁定的第一阶段:给 action body 的
ctx.session立契约 schema,按现状声明。零行为变更是硬边界——运行时产出的对象一个字节都没变。前提核实(对
origin/main@6ddb2ec4a)buildActionSession()在packages/runtime/src/action-execution.ts{ userId?, organizationId?, roles? },三键条件展开domains/actions.ts:333、action-execution.ts:985;http-dispatcher.ts里的同名注释是 D11③ 抽取后的残留文档,无第三处生产者actionContext都是裸anyconst actionContext: any = {×2ScriptContext.session?: unknown在sandbox/script-runner.ts一处需要精确化的前提(不推翻裁定,反而加强它)
#5613 正文第 3 条说 action ctx「没有任何 schema 声明」。Zod schema 确实一个都没有,但 spec 侧存在一个纯 TypeScript 接口的声明,而且形状完全吻合:
packages/spec/src/ui/action-params.zod.ts:167(改动前)它是
ActionHandlerContext的一个字段。三仓(objectstack / objectui / cloud)grep 确认ActionHandlerContext模块外零导入者。这处发现改变的不是结论而是落点:action ctx 的运行时声明已经有家了,只是那个家里只有类型没有 parse——而且带
[k: string]: unknown兜底,正是「declared ≠ enforced」的教科书形态。所以裁定「裸 any 面是漂移根因」成立且被这处证据加强:一个连兜底通配都写进去的类型面,和没有面在闸门眼里是一回事。schema 落点判定(premise-first,不是按 PM 提示)
派单提示「kernel/contracts 面」,实测后落在
packages/spec/src/ui/action-params.zod.ts,三条证据:ActionHandlerContext、ActionEngineFacade、ActionHandler。把 Zod schema 放到别处,等于给同一个对象立第二个家——正是本单要终结的漂移形态。HookContextSchema住在data/hook.zod.ts(挨着HookSchema),不在 kernel。action 的对应位置就是ui/的 action 族。action-execution.ts第 18 行本来就import { validateActionParams } from '@objectstack/spec/ui'——同一个模块。新类型跟着同一条 import 进来,没有新增依赖面。kernel/execution-context.zod.ts是生产者的信封(平台内部受众),把面向 action 作者的契约放那儿会把它和ActionHandlerContext拆开两处。改了什么
spec(新增契约)
ActionSessionSchema/ActionSession,按现状声明{ userId?: string, organizationId?: string, roles?: string[] }。HookContextSchema同姿态同理由:这是平台交给 body 的运行时形状、无人书写;关掉门会把将来引擎侧的一次补字段变成「拿到上下文的人 parse 失败」。roles的.describe()按派单要求标注 deprecated:值实为ExecutionContext.positions(ADR-0090 D3 词汇),声明现状不等于背书拼法;更名 + 弃用窗口 + ADR-0087 语义迁移属 [runtime] action body 的ctx.session仍在生产roles(值是ec.positions)—— 自称「mirroring hook ctx.session」,而 hook 侧该键已按 ADR-0049 退役 #5613 第二阶段;现在不引入positions键——在迁移前铸第二个拼法就是缺陷本身。同时写清:不要拿它判权限,判权限问 security service(ADR-0095)。ActionHandlerContext.session改指ActionSession,顺带去掉[k: string]: unknown兜底(三仓零导入者,narrow 不影响任何调用点;schema 仍非 strict,运行时多余键照样 parse,只是类型不再邀请作者去发明键)。build-docs.ts:把action-params放进Apps & Navigation分组,挨着action,而不是掉进More。runtime(仅类型标注 + pin,零行为变更)
buildActionSession()返回类型any | undefined→ActionSession | undefined。ScriptContext.session维持unknown,只补文档:这一个字段上同时走 hook 和 action 两种不同形状,收窄它会逼所有消费端去判别一个该类型并不携带的 body kind——那笔账不属于「声明现状」这一单,已在注释里点名留给后续。buildActionSession()的文档注释里那两句错话(「mirroring the hook ctx.session shape」、roles)故意留着不改,按裁定属第二阶段;但加了pin 与先证红(方向:预测为红,实测为红)
pin 落在
packages/runtime/src/action-session-shape-contract.test.ts——参照 #5668 的hook-input-shape-contract.test.ts:必须执行真 producer,而 spec 不能反向依赖 runtime。非 strict 的 schema 上,
safeParse().success单独是废的(多余键会被吞掉还判绿)。所以承重断言是parse(built)深等于built:非 strict parse 会 strip 未声明键,深等式因此正好等价于「producer 产出的每一个键都被契约声明了」。先按错误形状写并运行,预测两处红:
恰好两处,且同批其余断言全绿(它们是正控:pin 确实在跑,不是整文件炸掉)。翻正后全绿。
条件展开语义是实测的,不是假设
派单要求「measure, don't assume」。实测结论:
'organizationId' in ctx.session答false。这与 hook 侧 docs(spec): HookContext.input 契约表改成引擎真正构造的形状 (#5273) #5668 那个input.id「键在、值为undefined」是相反的(那边引擎用 shorthand 建对象),in测试不能在两者之间搬运。测试注释与 schema 文档都写死了这条。undefined而非{}(Approval: a schedule-triggered run still can't write its own locked record — it carries no ObjectQL context to holdflowRunId(#3456 residual) #3712 在 action 侧的对应事实)。推论:roles永远不会单独出现——只有 positions、没有 user 也没有 org 时,根本不产生 session 供它挂载。这条也 pin 住了。String()强转(数字 id 会被转成字符串,否则契约声明的string就是假话)、tenantId→organizationId的更名、以及 v11 移除的tenantId别名不复现。验证
--filter @objectstack/runtime test--filter @objectstack/spec test--filter @objectstack/spec typecheck--filter @objectstack/runtime typecheck--filter @objectstack/spec check:generatedcheck:exported-any/check:dual-source-exports/check:livenessnode scripts/check-nul-bytes.mjs全部在
flock /tmp/os-heavy-verify.lock下串行,--max-old-space-size=4096。生成物审阅(逐个,不是「跑一下就提交」)
packages/spec/api-surface.json:+2 行(ActionSession (type)/ActionSessionSchema (const))。packages/spec/authorable-surface.json:+3 键(ui/ActionSession:userId|organizationId|roles)。packages/spec/json-schema.manifest.json:+ui/ActionSession。content/docs/references/ui/action-params.mdx:新页——这个面第一次有生成参考页,roles的整段弃用处方直接渲染在表里。index.mdx/meta.json各 +1 行。z.object站点会被check:strictness-ledger双向要求登记,所以手写了两行(file-level triage 的wire判词 + remaining-strip 的out of scope行),counts.md由gen:strictness-ledger重算:ui/170→171 站点、49→50 strip、wire/open桶 +1。数字全部生成,判词手写。packages/spec/authorable-surface.base.json会被gen:schema重锚到当前 merge base,顺带带进别人已合并改动的api/Discovery:scoping等 2 键。已git checkout还原、不进本 PR;check:authorable-surface在还原后仍绿,并自己打印 “trails the merge base by 2 key(s) — expected right after a surface change lands”。没做的事
positions、不写 ADR-0087 条目——全是第二阶段([runtime] action body 的ctx.session仍在生产roles(值是ec.positions)—— 自称「mirroring hook ctx.session」,而 hook 侧该键已按 ADR-0049 退役 #5613)的活。conversions/*(ADR-0087 别名转换会把「被遮蔽的旧拼法」留在存量元数据里 —— 节点 config 收紧后它从静默丢弃变成执行期硬拒 #4923)、ui/component.zod.ts(RecordDetailsProps 与真实页面的授权形状不符:sections声明为 string[] 但所有页面授权对象形式,hideFields完全未声明 #5611)、data/hook.zod.ts([spec] HookContext.session 少声明了positions/preserveAudit—— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605)、api routes/rest(routes.mcp 是 REST /discovery 发出、objectui 真实消费、但 ApiRoutesSchema 从未声明的键(#4828 同族,低一层) #5679)。ScriptContext.session不收窄(理由见上)。changeset
.changeset/action-ctx-session-contract.md:@objectstack/specminor(新增导出 schema + type),@objectstack/runtimepatch(纯类型标注——但buildActionSession()的返回类型从any收窄为具体类型,对下游是可见的类型面变化,所以不按「无 changeset」处理)。Generated by Claude Code